From f1f7c63a5e72fa39af28189083db833c8284f087 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Wed, 5 Mar 2025 17:01:36 +0100 Subject: [PATCH 01/11] CM-45588 - Make batching more configurable and friendly in logs (#284) --- Dockerfile | 2 +- cycode/cli/commands/scan/code_scanner.py | 2 +- cycode/cli/consts.py | 4 ++ cycode/cli/utils/scan_batch.py | 77 +++++++++++++++++++++--- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 641b829d..8867d1f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ FROM base AS builder ENV POETRY_VERSION=1.8.3 # deps are required to build cffi -RUN apk add --no-cache --virtual .build-deps gcc=14.2.0-r4 libffi-dev=3.4.6-r0 musl-dev=1.2.5-r9 && \ +RUN apk add --no-cache --virtual .build-deps gcc=14.2.0-r4 libffi-dev=3.4.7-r0 musl-dev=1.2.5-r9 && \ pip install --no-cache-dir "poetry==$POETRY_VERSION" "poetry-dynamic-versioning[plugin]" && \ apk del .build-deps gcc libffi-dev musl-dev diff --git a/cycode/cli/commands/scan/code_scanner.py b/cycode/cli/commands/scan/code_scanner.py index b3fddf59..5f10ffdf 100644 --- a/cycode/cli/commands/scan/code_scanner.py +++ b/cycode/cli/commands/scan/code_scanner.py @@ -171,7 +171,7 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local should_use_sync_flow = _should_use_sync_flow(command_scan_type, scan_type, sync_option, scan_parameters) try: - logger.debug('Preparing local files, %s', {'batch_size': len(batch)}) + logger.debug('Preparing local files, %s', {'batch_files_count': len(batch)}) zipped_documents = zip_documents(scan_type, batch) zip_file_size = zipped_documents.size scan_result = perform_scan( diff --git a/cycode/cli/consts.py b/cycode/cli/consts.py index 558f5b7b..42bd1ab7 100644 --- a/cycode/cli/consts.py +++ b/cycode/cli/consts.py @@ -145,7 +145,11 @@ # scan in batches DEFAULT_SCAN_BATCH_MAX_SIZE_IN_BYTES = 9 * 1024 * 1024 SCAN_BATCH_MAX_SIZE_IN_BYTES = {SAST_SCAN_TYPE: 50 * 1024 * 1024} +SCAN_BATCH_MAX_SIZE_IN_BYTES_ENV_VAR_NAME = 'SCAN_BATCH_MAX_SIZE_IN_BYTES' + DEFAULT_SCAN_BATCH_MAX_FILES_COUNT = 1000 +SCAN_BATCH_MAX_FILES_COUNT_ENV_VAR_NAME = 'SCAN_BATCH_MAX_FILES_COUNT' + # if we increase this values, the server doesn't allow connecting (ConnectionError) SCAN_BATCH_MAX_PARALLEL_SCANS = 5 SCAN_BATCH_SCANS_PER_CPU = 1 diff --git a/cycode/cli/utils/scan_batch.py b/cycode/cli/utils/scan_batch.py index 3d2d83dc..4019b7b0 100644 --- a/cycode/cli/utils/scan_batch.py +++ b/cycode/cli/utils/scan_batch.py @@ -5,17 +5,53 @@ from cycode.cli import consts from cycode.cli.models import Document from cycode.cli.utils.progress_bar import ScanProgressBarSection +from cycode.cyclient import logger if TYPE_CHECKING: from cycode.cli.models import CliError, LocalScanResult from cycode.cli.utils.progress_bar import BaseProgressBar +def _get_max_batch_size(scan_type: str) -> int: + logger.debug( + 'You can customize the batch size by setting the environment variable "%s"', + consts.SCAN_BATCH_MAX_SIZE_IN_BYTES_ENV_VAR_NAME, + ) + + custom_size = os.environ.get(consts.SCAN_BATCH_MAX_SIZE_IN_BYTES_ENV_VAR_NAME) + if custom_size: + logger.debug('Custom batch size is set, %s', {'custom_size': custom_size}) + return int(custom_size) + + return consts.SCAN_BATCH_MAX_SIZE_IN_BYTES.get(scan_type, consts.DEFAULT_SCAN_BATCH_MAX_SIZE_IN_BYTES) + + +def _get_max_batch_files_count(_: str) -> int: + logger.debug( + 'You can customize the batch files count by setting the environment variable "%s"', + consts.SCAN_BATCH_MAX_FILES_COUNT_ENV_VAR_NAME, + ) + + custom_files_count = os.environ.get(consts.SCAN_BATCH_MAX_FILES_COUNT_ENV_VAR_NAME) + if custom_files_count: + logger.debug('Custom batch files count is set, %s', {'custom_files_count': custom_files_count}) + return int(custom_files_count) + + return consts.DEFAULT_SCAN_BATCH_MAX_FILES_COUNT + + def split_documents_into_batches( + scan_type: str, documents: List[Document], - max_size: int = consts.DEFAULT_SCAN_BATCH_MAX_SIZE_IN_BYTES, - max_files_count: int = consts.DEFAULT_SCAN_BATCH_MAX_FILES_COUNT, ) -> List[List[Document]]: + max_size = _get_max_batch_size(scan_type) + max_files_count = _get_max_batch_files_count(scan_type) + + logger.debug( + 'Splitting documents into batches, %s', + {'document_count': len(documents), 'max_batch_size': max_size, 'max_files_count': max_files_count}, + ) + batches = [] current_size = 0 @@ -23,7 +59,29 @@ def split_documents_into_batches( for document in documents: document_size = len(document.content.encode('UTF-8')) - if (current_size + document_size > max_size) or (len(current_batch) >= max_files_count): + exceeds_max_size = current_size + document_size > max_size + if exceeds_max_size: + logger.debug( + 'Going to create new batch because current batch size exceeds the limit, %s', + { + 'batch_index': len(batches), + 'current_batch_size': current_size + document_size, + 'max_batch_size': max_size, + }, + ) + + exceeds_max_files_count = len(current_batch) >= max_files_count + if exceeds_max_files_count: + logger.debug( + 'Going to create new batch because current batch files count exceeds the limit, %s', + { + 'batch_index': len(batches), + 'current_batch_files_count': len(current_batch), + 'max_batch_files_count': max_files_count, + }, + ) + + if exceeds_max_size or exceeds_max_files_count: batches.append(current_batch) current_batch = [document] @@ -35,6 +93,8 @@ def split_documents_into_batches( if current_batch: batches.append(current_batch) + logger.debug('Documents were split into batches %s', {'batches_count': len(batches)}) + return batches @@ -49,9 +109,8 @@ def run_parallel_batched_scan( documents: List[Document], progress_bar: 'BaseProgressBar', ) -> Tuple[Dict[str, 'CliError'], List['LocalScanResult']]: - max_size = consts.SCAN_BATCH_MAX_SIZE_IN_BYTES.get(scan_type, consts.DEFAULT_SCAN_BATCH_MAX_SIZE_IN_BYTES) - - batches = [documents] if scan_type == consts.SCA_SCAN_TYPE else split_documents_into_batches(documents, max_size) + # batching is disabled for SCA; requested by Mor + batches = [documents] if scan_type == consts.SCA_SCAN_TYPE else split_documents_into_batches(scan_type, documents) progress_bar.set_section_length(ScanProgressBarSection.SCAN, len(batches)) # * 3 # TODO(MarshalX): we should multiply the count of batches in SCAN section because each batch has 3 steps: @@ -61,9 +120,13 @@ def run_parallel_batched_scan( # it's not possible yet because not all scan types moved to polling mechanism # the progress bar could be significant improved (be more dynamic) in the future + threads_count = _get_threads_count() local_scan_results: List['LocalScanResult'] = [] cli_errors: Dict[str, 'CliError'] = {} - with ThreadPool(processes=_get_threads_count()) as pool: + + logger.debug('Running parallel batched scan, %s', {'threads_count': threads_count, 'batches_count': len(batches)}) + + with ThreadPool(processes=threads_count) as pool: for scan_id, err, result in pool.imap(scan_function, batches): if result: local_scan_results.append(result) From 27ca8631a45dd94f4aad41b1919cbc390628119e Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Fri, 21 Mar 2025 12:54:12 +0100 Subject: [PATCH 02/11] CM-46055 - Fix scan parameters (#288) --- cycode/cli/commands/scan/code_scanner.py | 126 ++++++++++-------- .../scan/pre_commit/pre_commit_command.py | 4 +- .../scan/repository/repository_command.py | 3 +- tests/test_code_scanner.py | 4 +- 4 files changed, 72 insertions(+), 65 deletions(-) diff --git a/cycode/cli/commands/scan/code_scanner.py b/cycode/cli/commands/scan/code_scanner.py index 5f10ffdf..0dbf63ea 100644 --- a/cycode/cli/commands/scan/code_scanner.py +++ b/cycode/cli/commands/scan/code_scanner.py @@ -45,7 +45,7 @@ def scan_sca_pre_commit(context: click.Context) -> None: scan_type = context.obj['scan_type'] - scan_parameters = get_default_scan_parameters(context) + scan_parameters = get_scan_parameters(context) git_head_documents, pre_committed_documents = get_pre_commit_modified_documents( context.obj['progress_bar'], ScanProgressBarSection.PREPARE_LOCAL_FILES ) @@ -80,14 +80,13 @@ def scan_sca_commit_range(context: click.Context, path: str, commit_range: str) def scan_disk_files(context: click.Context, paths: Tuple[str]) -> None: - scan_parameters = get_scan_parameters(context, paths) scan_type = context.obj['scan_type'] progress_bar = context.obj['progress_bar'] try: documents = get_relevant_documents(progress_bar, ScanProgressBarSection.PREPARE_LOCAL_FILES, scan_type, paths) perform_pre_scan_documents_actions(context, scan_type, documents) - scan_documents(context, documents, scan_parameters=scan_parameters) + scan_documents(context, documents, get_scan_parameters(context, paths)) except Exception as e: handle_scan_exception(context, e) @@ -151,14 +150,12 @@ def _enrich_scan_result_with_data_from_detection_rules( def _get_scan_documents_thread_func( context: click.Context, is_git_diff: bool, is_commit_range: bool, scan_parameters: dict -) -> Tuple[Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]], str]: +) -> Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]]: cycode_client = context.obj['client'] scan_type = context.obj['scan_type'] severity_threshold = context.obj['severity_threshold'] sync_option = context.obj['sync'] command_scan_type = context.info_name - aggregation_id = str(_generate_unique_id()) - scan_parameters['aggregation_id'] = aggregation_id def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, LocalScanResult]: local_scan_result = error = error_message = None @@ -227,7 +224,7 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local return scan_id, error, local_scan_result - return _scan_batch_thread_func, aggregation_id + return _scan_batch_thread_func def scan_commit_range( @@ -287,20 +284,19 @@ def scan_commit_range( logger.debug('List of commit ids to scan, %s', {'commit_ids': commit_ids_to_scan}) logger.debug('Starting to scan commit range (it may take a few minutes)') - scan_documents(context, documents_to_scan, is_git_diff=True, is_commit_range=True) + scan_documents( + context, documents_to_scan, get_scan_parameters(context, (path,)), is_git_diff=True, is_commit_range=True + ) return None def scan_documents( context: click.Context, documents_to_scan: List[Document], + scan_parameters: dict, is_git_diff: bool = False, is_commit_range: bool = False, - scan_parameters: Optional[dict] = None, ) -> None: - if not scan_parameters: - scan_parameters = get_default_scan_parameters(context) - scan_type = context.obj['scan_type'] progress_bar = context.obj['progress_bar'] @@ -315,19 +311,15 @@ def scan_documents( ) return - scan_batch_thread_func, aggregation_id = _get_scan_documents_thread_func( - context, is_git_diff, is_commit_range, scan_parameters - ) + scan_batch_thread_func = _get_scan_documents_thread_func(context, is_git_diff, is_commit_range, scan_parameters) errors, local_scan_results = run_parallel_batched_scan( scan_batch_thread_func, scan_type, documents_to_scan, progress_bar=progress_bar ) - if len(local_scan_results) > 1: - # if we used more than one batch, we need to fetch aggregate report url - aggregation_report_url = _try_get_aggregation_report_url_if_needed( - scan_parameters, context.obj['client'], scan_type - ) - set_aggregation_report_url(context, aggregation_report_url) + aggregation_report_url = _try_get_aggregation_report_url_if_needed( + scan_parameters, context.obj['client'], scan_type + ) + _set_aggregation_report_url(context, aggregation_report_url) progress_bar.set_section_length(ScanProgressBarSection.GENERATE_REPORT, 1) progress_bar.update(ScanProgressBarSection.GENERATE_REPORT) @@ -337,25 +329,6 @@ def scan_documents( print_results(context, local_scan_results, errors) -def set_aggregation_report_url(context: click.Context, aggregation_report_url: Optional[str] = None) -> None: - context.obj['aggregation_report_url'] = aggregation_report_url - - -def _try_get_aggregation_report_url_if_needed( - scan_parameters: dict, cycode_client: 'ScanClient', scan_type: str -) -> Optional[str]: - aggregation_id = scan_parameters.get('aggregation_id') - if not scan_parameters.get('report'): - return None - if aggregation_id is None: - return None - try: - report_url_response = cycode_client.get_scan_aggregation_report_url(aggregation_id, scan_type) - return report_url_response.report_url - except Exception as e: - logger.debug('Failed to get aggregation report url: %s', str(e)) - - def scan_commit_range_documents( context: click.Context, from_documents_to_scan: List[Document], @@ -380,7 +353,7 @@ def scan_commit_range_documents( try: progress_bar.set_section_length(ScanProgressBarSection.SCAN, 1) - scan_result = init_default_scan_result(cycode_client, scan_id, scan_type) + scan_result = init_default_scan_result(scan_id) if should_scan_documents(from_documents_to_scan, to_documents_to_scan): logger.debug('Preparing from-commit zip') from_commit_zipped_documents = zip_documents(scan_type, from_documents_to_scan) @@ -518,7 +491,7 @@ def perform_scan_async( cycode_client, scan_async_result.scan_id, scan_type, - scan_parameters.get('report'), + scan_parameters, ) @@ -553,16 +526,14 @@ def perform_commit_range_scan_async( logger.debug( 'Async commit range scan request has been triggered successfully, %s', {'scan_id': scan_async_result.scan_id} ) - return poll_scan_results( - cycode_client, scan_async_result.scan_id, scan_type, scan_parameters.get('report'), timeout - ) + return poll_scan_results(cycode_client, scan_async_result.scan_id, scan_type, scan_parameters, timeout) def poll_scan_results( cycode_client: 'ScanClient', scan_id: str, scan_type: str, - should_get_report: bool = False, + scan_parameters: dict, polling_timeout: Optional[int] = None, ) -> ZippedFileScanResult: if polling_timeout is None: @@ -579,7 +550,7 @@ def poll_scan_results( print_debug_scan_details(scan_details) if scan_details.scan_status == consts.SCAN_STATUS_COMPLETED: - return _get_scan_result(cycode_client, scan_type, scan_id, scan_details, should_get_report) + return _get_scan_result(cycode_client, scan_type, scan_id, scan_details, scan_parameters) if scan_details.scan_status == consts.SCAN_STATUS_ERROR: raise custom_exceptions.ScanAsyncError( @@ -671,18 +642,19 @@ def parse_pre_receive_input() -> str: return pre_receive_input.splitlines()[0] -def get_default_scan_parameters(context: click.Context) -> dict: +def _get_default_scan_parameters(context: click.Context) -> dict: return { 'monitor': context.obj.get('monitor'), 'report': context.obj.get('report'), 'package_vulnerabilities': context.obj.get('package-vulnerabilities'), 'license_compliance': context.obj.get('license-compliance'), 'command_type': context.info_name, + 'aggregation_id': str(_generate_unique_id()), } -def get_scan_parameters(context: click.Context, paths: Tuple[str]) -> dict: - scan_parameters = get_default_scan_parameters(context) +def get_scan_parameters(context: click.Context, paths: Optional[Tuple[str]] = None) -> dict: + scan_parameters = _get_default_scan_parameters(context) if not paths: return scan_parameters @@ -890,10 +862,10 @@ def _get_scan_result( scan_type: str, scan_id: str, scan_details: 'ScanDetailsResponse', - should_get_report: bool = False, + scan_parameters: dict, ) -> ZippedFileScanResult: if not scan_details.detections_count: - return init_default_scan_result(cycode_client, scan_id, scan_type, should_get_report) + return init_default_scan_result(scan_id) scan_raw_detections = cycode_client.get_scan_raw_detections(scan_type, scan_id) @@ -901,25 +873,40 @@ def _get_scan_result( did_detect=True, detections_per_file=_map_detections_per_file_and_commit_id(scan_type, scan_raw_detections), scan_id=scan_id, - report_url=_try_get_report_url_if_needed(cycode_client, should_get_report, scan_id, scan_type), + report_url=_try_get_any_report_url_if_needed(cycode_client, scan_id, scan_type, scan_parameters), ) -def init_default_scan_result( - cycode_client: 'ScanClient', scan_id: str, scan_type: str, should_get_report: bool = False -) -> ZippedFileScanResult: +def init_default_scan_result(scan_id: str) -> ZippedFileScanResult: return ZippedFileScanResult( did_detect=False, detections_per_file=[], scan_id=scan_id, - report_url=_try_get_report_url_if_needed(cycode_client, should_get_report, scan_id, scan_type), ) +def _try_get_any_report_url_if_needed( + cycode_client: 'ScanClient', + scan_id: str, + scan_type: str, + scan_parameters: dict, +) -> Optional[str]: + """Tries to get aggregation report URL if needed, otherwise tries to get report URL.""" + aggregation_report_url = None + if scan_parameters: + _try_get_report_url_if_needed(cycode_client, scan_id, scan_type, scan_parameters) + aggregation_report_url = _try_get_aggregation_report_url_if_needed(scan_parameters, cycode_client, scan_type) + + if aggregation_report_url: + return aggregation_report_url + + return _try_get_report_url_if_needed(cycode_client, scan_id, scan_type, scan_parameters) + + def _try_get_report_url_if_needed( - cycode_client: 'ScanClient', should_get_report: bool, scan_id: str, scan_type: str + cycode_client: 'ScanClient', scan_id: str, scan_type: str, scan_parameters: dict ) -> Optional[str]: - if not should_get_report: + if not scan_parameters.get('report', False): return None try: @@ -929,6 +916,27 @@ def _try_get_report_url_if_needed( logger.debug('Failed to get report URL', exc_info=e) +def _set_aggregation_report_url(context: click.Context, aggregation_report_url: Optional[str] = None) -> None: + context.obj['aggregation_report_url'] = aggregation_report_url + + +def _try_get_aggregation_report_url_if_needed( + scan_parameters: dict, cycode_client: 'ScanClient', scan_type: str +) -> Optional[str]: + if not scan_parameters.get('report', False): + return None + + aggregation_id = scan_parameters.get('aggregation_id') + if aggregation_id is None: + return None + + try: + report_url_response = cycode_client.get_scan_aggregation_report_url(aggregation_id, scan_type) + return report_url_response.report_url + except Exception as e: + logger.debug('Failed to get aggregation report url: %s', str(e)) + + def _map_detections_per_file_and_commit_id(scan_type: str, raw_detections: List[dict]) -> List[DetectionsPerFile]: """Converts list of detections (async flow) to list of DetectionsPerFile objects (sync flow). diff --git a/cycode/cli/commands/scan/pre_commit/pre_commit_command.py b/cycode/cli/commands/scan/pre_commit/pre_commit_command.py index fa4b295a..e71f2772 100644 --- a/cycode/cli/commands/scan/pre_commit/pre_commit_command.py +++ b/cycode/cli/commands/scan/pre_commit/pre_commit_command.py @@ -4,7 +4,7 @@ import click from cycode.cli import consts -from cycode.cli.commands.scan.code_scanner import scan_documents, scan_sca_pre_commit +from cycode.cli.commands.scan.code_scanner import get_scan_parameters, scan_documents, scan_sca_pre_commit from cycode.cli.files_collector.excluder import exclude_irrelevant_documents_to_scan from cycode.cli.files_collector.repository_documents import ( get_diff_file_content, @@ -44,4 +44,4 @@ def pre_commit_command(context: click.Context, ignored_args: List[str]) -> None: documents_to_scan.append(Document(get_path_by_os(get_diff_file_path(file)), get_diff_file_content(file))) documents_to_scan = exclude_irrelevant_documents_to_scan(scan_type, documents_to_scan) - scan_documents(context, documents_to_scan, is_git_diff=True) + scan_documents(context, documents_to_scan, get_scan_parameters(context), is_git_diff=True) diff --git a/cycode/cli/commands/scan/repository/repository_command.py b/cycode/cli/commands/scan/repository/repository_command.py index 9485c31c..b0a0effb 100644 --- a/cycode/cli/commands/scan/repository/repository_command.py +++ b/cycode/cli/commands/scan/repository/repository_command.py @@ -63,7 +63,6 @@ def repository_command(context: click.Context, path: str, branch: str) -> None: perform_pre_scan_documents_actions(context, scan_type, documents_to_scan) logger.debug('Found all relevant files for scanning %s', {'path': path, 'branch': branch}) - scan_parameters = get_scan_parameters(context, (path,)) - scan_documents(context, documents_to_scan, scan_parameters=scan_parameters) + scan_documents(context, documents_to_scan, get_scan_parameters(context, (path,))) except Exception as e: handle_scan_exception(context, e) diff --git a/tests/test_code_scanner.py b/tests/test_code_scanner.py index 10726a65..d0ae939a 100644 --- a/tests/test_code_scanner.py +++ b/tests/test_code_scanner.py @@ -29,7 +29,7 @@ def test_is_relevant_file_to_scan_sca() -> None: @pytest.mark.parametrize('scan_type', config['scans']['supported_scans']) def test_try_get_report_url_if_needed_return_none(scan_type: str, scan_client: ScanClient) -> None: scan_id = uuid4().hex - result = _try_get_report_url_if_needed(scan_client, False, scan_id, consts.SECRET_SCAN_TYPE) + result = _try_get_report_url_if_needed(scan_client, scan_id, consts.SECRET_SCAN_TYPE, scan_parameters={}) assert result is None @@ -44,7 +44,7 @@ def test_try_get_report_url_if_needed_return_result( responses.add(get_scan_report_url_response(url, scan_id)) scan_report_url_response = scan_client.get_scan_report_url(str(scan_id), scan_type) - result = _try_get_report_url_if_needed(scan_client, True, str(scan_id), scan_type) + result = _try_get_report_url_if_needed(scan_client, str(scan_id), scan_type, scan_parameters={'report': True}) assert result == scan_report_url_response.report_url From 2901f82d5b7cc7f1ed53d3b9c904251677e6a6ec Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Fri, 4 Apr 2025 15:12:38 +0200 Subject: [PATCH 03/11] CM-46426 - Fix severity for SCA (use Cycode severity instead of Advisory Severity) (#292) --- cycode/cli/commands/scan/code_scanner.py | 4 +--- cycode/cli/printers/tables/sca_table_printer.py | 7 +++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cycode/cli/commands/scan/code_scanner.py b/cycode/cli/commands/scan/code_scanner.py index 0dbf63ea..478b8ffe 100644 --- a/cycode/cli/commands/scan/code_scanner.py +++ b/cycode/cli/commands/scan/code_scanner.py @@ -695,9 +695,7 @@ def exclude_irrelevant_detections( def _exclude_detections_by_severity(detections: List[Detection], severity_threshold: str) -> List[Detection]: relevant_detections = [] for detection in detections: - severity = detection.detection_details.get('advisory_severity') - if not severity: - severity = detection.severity + severity = detection.severity if _does_severity_match_severity_threshold(severity, severity_threshold): relevant_detections.append(detection) diff --git a/cycode/cli/printers/tables/sca_table_printer.py b/cycode/cli/printers/tables/sca_table_printer.py index 5a6ec726..e92b2be7 100644 --- a/cycode/cli/printers/tables/sca_table_printer.py +++ b/cycode/cli/printers/tables/sca_table_printer.py @@ -72,9 +72,8 @@ def __group_by(detections: List[Detection], details_field_name: str) -> Dict[str @staticmethod def __severity_sort_key(detection: Detection) -> int: - severity = detection.detection_details.get('advisory_severity') - if severity: - return Severity.get_member_weight(severity) + if detection.severity: + return Severity.get_member_weight(detection.severity) return SEVERITY_UNKNOWN_WEIGHT @@ -138,7 +137,7 @@ def _get_table(self, policy_id: str) -> Table: def _enrich_table_with_values(table: Table, detection: Detection) -> None: detection_details = detection.detection_details - table.set(SEVERITY_COLUMN, detection_details.get('advisory_severity')) + table.set(SEVERITY_COLUMN, detection.severity) table.set(REPOSITORY_COLUMN, detection_details.get('repository_name')) table.set(CODE_PROJECT_COLUMN, detection_details.get('file_name')) From a1c7a4f8266e97018178aa4eb50f672758d461f3 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Wed, 23 Apr 2025 16:58:50 +0200 Subject: [PATCH 04/11] CM-47505 - Add support for Unity Version Control (formerly Plastic SCM) (#298) --- cycode/cli/commands/scan/code_scanner.py | 91 ++++++++++++++++++++++++ cycode/cli/consts.py | 4 ++ 2 files changed, 95 insertions(+) diff --git a/cycode/cli/commands/scan/code_scanner.py b/cycode/cli/commands/scan/code_scanner.py index 478b8ffe..4091118f 100644 --- a/cycode/cli/commands/scan/code_scanner.py +++ b/cycode/cli/commands/scan/code_scanner.py @@ -32,6 +32,7 @@ from cycode.cli.utils.progress_bar import ScanProgressBarSection from cycode.cli.utils.scan_batch import run_parallel_batched_scan from cycode.cli.utils.scan_utils import set_issue_detected +from cycode.cli.utils.shell_executor import shell from cycode.cyclient import logger from cycode.cyclient.config import set_logging_level from cycode.cyclient.models import Detection, DetectionSchema, DetectionsPerFile, ZippedFileScanResult @@ -666,6 +667,9 @@ def get_scan_parameters(context: click.Context, paths: Optional[Tuple[str]] = No return scan_parameters remote_url = try_get_git_remote_url(paths[0]) + if not remote_url: + remote_url = try_to_get_plastic_remote_url(paths[0]) + if remote_url: # TODO(MarshalX): remove hardcode in context context.obj['remote_url'] = remote_url @@ -684,6 +688,93 @@ def try_get_git_remote_url(path: str) -> Optional[str]: return None +def _get_plastic_repository_name(path: str) -> Optional[str]: + """Gets the name of the Plastic repository from the current working directory. + + The command to execute is: + cm status --header --machinereadable --fieldseparator=":::" + + Example of status header in machine-readable format: + STATUS:::0:::Project/RepoName:::OrgName@ServerInfo + """ + + try: + command = [ + 'cm', + 'status', + '--header', + '--machinereadable', + f'--fieldseparator={consts.PLASTIC_VCS_DATA_SEPARATOR}', + ] + + status = shell(command=command, timeout=consts.PLASTIC_VSC_CLI_TIMEOUT, working_directory=path) + if not status: + logger.debug('Failed to get Plastic repository name (command failed)') + return None + + status_parts = status.split(consts.PLASTIC_VCS_DATA_SEPARATOR) + if len(status_parts) < 2: + logger.debug('Failed to parse Plastic repository name (command returned unexpected format)') + return None + + return status_parts[2].strip() + except Exception as e: + logger.debug('Failed to get Plastic repository name', exc_info=e) + return None + + +def _get_plastic_repository_list(working_dir: Optional[str] = None) -> Dict[str, str]: + """Gets the list of Plastic repositories and their GUIDs. + + The command to execute is: + cm repo list --format="{repname}:::{repguid}" + + Example line with data: + Project/RepoName:::tapo1zqt-wn99-4752-h61m-7d9k79d40r4v + + Each line represents an individual repository. + """ + + repo_name_to_guid = {} + + try: + command = ['cm', 'repo', 'ls', f'--format={{repname}}{consts.PLASTIC_VCS_DATA_SEPARATOR}{{repguid}}'] + + status = shell(command=command, timeout=consts.PLASTIC_VSC_CLI_TIMEOUT, working_directory=working_dir) + if not status: + logger.debug('Failed to get Plastic repository list (command failed)') + return repo_name_to_guid + + status_lines = status.splitlines() + for line in status_lines: + data_parts = line.split(consts.PLASTIC_VCS_DATA_SEPARATOR) + if len(data_parts) < 2: + logger.debug('Failed to parse Plastic repository list line (unexpected format), %s', {'line': line}) + continue + + repo_name, repo_guid = data_parts + repo_name_to_guid[repo_name.strip()] = repo_guid.strip() + + return repo_name_to_guid + except Exception as e: + logger.debug('Failed to get Plastic repository list', exc_info=e) + return repo_name_to_guid + + +def try_to_get_plastic_remote_url(path: str) -> Optional[str]: + repository_name = _get_plastic_repository_name(path) + if not repository_name: + return None + + repository_map = _get_plastic_repository_list(path) + if repository_name not in repository_map: + logger.debug('Failed to get Plastic repository GUID (repository not found in the list)') + return None + + repository_guid = repository_map[repository_name] + return f'{consts.PLASTIC_VCS_REMOTE_URI_PREFIX}{repository_guid}' + + def exclude_irrelevant_detections( detections: List[Detection], scan_type: str, command_scan_type: str, severity_threshold: str ) -> List[Detection]: diff --git a/cycode/cli/consts.py b/cycode/cli/consts.py index 42bd1ab7..003218d6 100644 --- a/cycode/cli/consts.py +++ b/cycode/cli/consts.py @@ -230,3 +230,7 @@ SCA_SKIP_RESTORE_DEPENDENCIES_FLAG = 'no-restore' SCA_GRADLE_ALL_SUB_PROJECTS_FLAG = 'gradle-all-sub-projects' + +PLASTIC_VCS_DATA_SEPARATOR = ':::' +PLASTIC_VSC_CLI_TIMEOUT = 10 +PLASTIC_VCS_REMOTE_URI_PREFIX = 'plastic::' From 4681d3268140494f10e16a701739a188b2ba513a Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Fri, 25 Apr 2025 14:10:33 +0200 Subject: [PATCH 05/11] CM-47698 - Fix Windows signing of CLI executable (#300) --- .github/workflows/build_executable.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_executable.yml b/.github/workflows/build_executable.yml index 44c9a02a..41cfa2ed 100644 --- a/.github/workflows/build_executable.yml +++ b/.github/workflows/build_executable.yml @@ -166,6 +166,7 @@ jobs: shell: cmd env: SM_HOST: ${{ secrets.SM_HOST }} + SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} SM_API_KEY: ${{ secrets.SM_API_KEY }} SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} @@ -174,7 +175,7 @@ jobs: curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi msiexec /i smtools-windows-x64.msi /quiet /qn C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user - smksp_cert_sync.exe + smctl windows certsync --keypair-alias=%SM_KEYPAIR_ALIAS% :: sign executable signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ".\dist\cycode-cli.exe" From 8c69a5c16e3c2dc7fe748662ee18c00aceb6c704 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Fri, 25 Apr 2025 14:44:29 +0200 Subject: [PATCH 06/11] fixes after main merge --- cycode/cli/printers/utils/detection_ordering/common_ordering.py | 2 +- cycode/cli/printers/utils/detection_ordering/sca_ordering.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cycode/cli/printers/utils/detection_ordering/common_ordering.py b/cycode/cli/printers/utils/detection_ordering/common_ordering.py index d93b858e..c0f8dddd 100644 --- a/cycode/cli/printers/utils/detection_ordering/common_ordering.py +++ b/cycode/cli/printers/utils/detection_ordering/common_ordering.py @@ -36,7 +36,7 @@ def _sort_detections_by_file_path( def sort_and_group_detections( detections_with_documents: List[Tuple['Detection', 'Document']], ) -> GroupedDetections: - """Sort detections by severity. We do not have groping here (don't find the best one yet).""" + """Sort detections by severity. We do not have grouping here (don't find the best one yet).""" group_separator_indexes = set() # we sort detections by file path to make persist output order diff --git a/cycode/cli/printers/utils/detection_ordering/sca_ordering.py b/cycode/cli/printers/utils/detection_ordering/sca_ordering.py index 85915c56..3886f2c7 100644 --- a/cycode/cli/printers/utils/detection_ordering/sca_ordering.py +++ b/cycode/cli/printers/utils/detection_ordering/sca_ordering.py @@ -15,7 +15,7 @@ def __group_by(detections: List['Detection'], details_field_name: str) -> Dict[s def __severity_sort_key(detection: 'Detection') -> int: - severity = detection.detection_details.get('advisory_severity', 'unknown') + severity = detection.severity if detection.severity else 'unknown' return SeverityOption.get_member_weight(severity) From 8884a4f4e1fb9d5c28e0371566f633e9cd7f8f5e Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Mon, 28 Apr 2025 12:46:49 +0200 Subject: [PATCH 07/11] add help for commands; add epilog; remove "auth check" command; fix ignore command --- cycode/cli/apps/ai_remediation/__init__.py | 15 ++++++++++-- .../ai_remediation/ai_remediation_command.py | 9 ++++++- cycode/cli/apps/auth/__init__.py | 20 +++++++++------- cycode/cli/apps/auth/auth_command.py | 14 +++++++---- cycode/cli/apps/auth/auth_common.py | 9 +++---- cycode/cli/apps/auth/check_command.py | 24 ------------------- cycode/cli/apps/configure/__init__.py | 17 ++++++++++--- .../cli/apps/configure/configure_command.py | 15 +++++++++++- cycode/cli/apps/ignore/ignore_command.py | 17 +++++++++++-- cycode/cli/apps/report/report_command.py | 6 ++++- cycode/cli/apps/scan/__init__.py | 11 ++++++++- .../commit_history/commit_history_command.py | 2 +- .../scan/repository/repository_command.py | 2 +- cycode/cli/apps/scan/scan_command.py | 18 +++++++++++--- cycode/cli/apps/status/get_cli_status.py | 8 +++++-- cycode/cli/apps/status/status_command.py | 18 +++++++++++++- cycode/cli/exceptions/handle_scan_errors.py | 2 +- cycode/cli/utils/git_proxy.py | 2 +- 18 files changed, 146 insertions(+), 63 deletions(-) delete mode 100644 cycode/cli/apps/auth/check_command.py diff --git a/cycode/cli/apps/ai_remediation/__init__.py b/cycode/cli/apps/ai_remediation/__init__.py index 0f017cf7..cd471a08 100644 --- a/cycode/cli/apps/ai_remediation/__init__.py +++ b/cycode/cli/apps/ai_remediation/__init__.py @@ -2,8 +2,19 @@ from cycode.cli.apps.ai_remediation.ai_remediation_command import ai_remediation_command -app = typer.Typer(no_args_is_help=True) -app.command(name='ai-remediation', short_help='Get AI remediation (INTERNAL).', hidden=True)(ai_remediation_command) +app = typer.Typer() + +_ai_remediation_epilog = """ +Note: AI remediation suggestions are generated automatically and should be reviewed before applying. +""" + +app.command( + name='ai-remediation', + short_help='Get AI remediation (INTERNAL).', + epilog=_ai_remediation_epilog, + hidden=True, + no_args_is_help=True, +)(ai_remediation_command) # backward compatibility app.command(hidden=True, name='ai_remediation')(ai_remediation_command) diff --git a/cycode/cli/apps/ai_remediation/ai_remediation_command.py b/cycode/cli/apps/ai_remediation/ai_remediation_command.py index 0a82b815..ea5ef826 100644 --- a/cycode/cli/apps/ai_remediation/ai_remediation_command.py +++ b/cycode/cli/apps/ai_remediation/ai_remediation_command.py @@ -16,7 +16,14 @@ def ai_remediation_command( bool, typer.Option('--fix', help='Apply fixes to resolve violations. Note: fix could be not available.') ] = False, ) -> None: - """Get AI remediation (INTERNAL).""" + """:robot: [bold cyan]Get AI-powered remediation for security issues.[/] + + This command provides AI-generated remediation guidance for detected security issues. + + Example usage: + * `cycode ai-remediation `: View remediation guidance + * `cycode ai-remediation --fix`: Apply suggested fixes + """ client = get_scan_cycode_client() try: diff --git a/cycode/cli/apps/auth/__init__.py b/cycode/cli/apps/auth/__init__.py index 951a9f1f..beecae38 100644 --- a/cycode/cli/apps/auth/__init__.py +++ b/cycode/cli/apps/auth/__init__.py @@ -1,12 +1,14 @@ import typer from cycode.cli.apps.auth.auth_command import auth_command -from cycode.cli.apps.auth.check_command import check_command - -app = typer.Typer( - name='auth', - help='Authenticate your machine to associate the CLI with your Cycode account.', - no_args_is_help=True, -) -app.callback(invoke_without_command=True)(auth_command) -app.command(name='check')(check_command) + +_auth_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#using-the-auth-command' +_auth_command_epilog = f"""[bold]Documentation[/] + + + +For more details and advanced usage, visit: [link={_auth_command_docs}]{_auth_command_docs}[/link] +""" + +app = typer.Typer(no_args_is_help=False) +app.command(name='auth', epilog=_auth_command_epilog, short_help='Authenticate your machine with Cycode.')(auth_command) diff --git a/cycode/cli/apps/auth/auth_command.py b/cycode/cli/apps/auth/auth_command.py index a402b0c2..bf577ec8 100644 --- a/cycode/cli/apps/auth/auth_command.py +++ b/cycode/cli/apps/auth/auth_command.py @@ -8,14 +8,18 @@ def auth_command(ctx: typer.Context) -> None: - """Authenticates your machine.""" + """:key: [bold cyan]Authenticate your machine with Cycode.[/] + + This command handles authentication with Cycode's security platform. + + Example usage: + * `cycode auth`: Start interactive authentication + * `cycode auth --help`: View authentication options + """ + add_breadcrumb('auth') printer = ctx.obj.get('console_printer') - if ctx.invoked_subcommand is not None: - # if it is a subcommand, do nothing - return - try: logger.debug('Starting authentication process') diff --git a/cycode/cli/apps/auth/auth_common.py b/cycode/cli/apps/auth/auth_common.py index f6120d94..52b7b6fa 100644 --- a/cycode/cli/apps/auth/auth_common.py +++ b/cycode/cli/apps/auth/auth_common.py @@ -1,6 +1,4 @@ -from typing import Optional - -import typer +from typing import TYPE_CHECKING, Optional from cycode.cli.apps.auth.models import AuthInfo from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError @@ -8,8 +6,11 @@ from cycode.cli.utils.jwt_utils import get_user_and_tenant_ids_from_access_token from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient +if TYPE_CHECKING: + from typer import Context + -def get_authorization_info(ctx: Optional[typer.Context] = None) -> Optional[AuthInfo]: +def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]: printer = ctx.obj.get('console_printer') client_id, client_secret = CredentialsManager().get_credentials() diff --git a/cycode/cli/apps/auth/check_command.py b/cycode/cli/apps/auth/check_command.py deleted file mode 100644 index 0a5ea5b3..00000000 --- a/cycode/cli/apps/auth/check_command.py +++ /dev/null @@ -1,24 +0,0 @@ -import typer - -from cycode.cli.apps.auth.auth_common import get_authorization_info -from cycode.cli.models import CliResult -from cycode.cli.utils.sentry import add_breadcrumb - - -def check_command(ctx: typer.Context) -> None: - """Checks that your machine is associating the CLI with your Cycode account.""" - add_breadcrumb('check') - - printer = ctx.obj.get('console_printer') - auth_info = get_authorization_info(ctx) - if auth_info is None: - printer.print_result(CliResult(success=False, message='Cycode authentication failed')) - return - - printer.print_result( - CliResult( - success=True, - message='Cycode authentication verified', - data={'user_id': auth_info.user_id, 'tenant_id': auth_info.tenant_id}, - ) - ) diff --git a/cycode/cli/apps/configure/__init__.py b/cycode/cli/apps/configure/__init__.py index 039c6f2e..ce73c450 100644 --- a/cycode/cli/apps/configure/__init__.py +++ b/cycode/cli/apps/configure/__init__.py @@ -2,7 +2,18 @@ from cycode.cli.apps.configure.configure_command import configure_command +_configure_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#using-the-configure-command' +_configure_command_epilog = f"""[bold]Documentation[/] + + + +For more details and advanced usage, visit: [link={_configure_command_docs}]{_configure_command_docs}[/link] +""" + + app = typer.Typer(no_args_is_help=True) -app.command(name='configure', short_help='Initial command to configure your CLI client authentication.')( - configure_command -) +app.command( + name='configure', + epilog=_configure_command_epilog, + short_help='Initial command to configure your CLI client authentication.', +)(configure_command) diff --git a/cycode/cli/apps/configure/configure_command.py b/cycode/cli/apps/configure/configure_command.py index 2aa86a8f..348e3ccb 100644 --- a/cycode/cli/apps/configure/configure_command.py +++ b/cycode/cli/apps/configure/configure_command.py @@ -23,7 +23,20 @@ def _should_update_value( def configure_command() -> None: - """Configure your CLI client authentication manually.""" + """:gear: [bold cyan]Configure Cycode CLI settings.[/] + + This command allows you to configure various aspects of the Cycode CLI. + + Configuration options: + * API URL: The base URL for Cycode's API (for on-premise or EU installations) + * APP URL: The base URL for Cycode's web application (for on-premise or EU installations) + * Client ID: Your Cycode client ID for authentication + * Client Secret: Your Cycode client secret for authentication + + Example usage: + * `cycode configure`: Start interactive configuration + * `cycode configure --help`: View configuration options + """ add_breadcrumb('configure') global_config_manager = CONFIGURATION_MANAGER.global_config_file_manager diff --git a/cycode/cli/apps/ignore/ignore_command.py b/cycode/cli/apps/ignore/ignore_command.py index 079a3c2d..1183114a 100644 --- a/cycode/cli/apps/ignore/ignore_command.py +++ b/cycode/cli/apps/ignore/ignore_command.py @@ -83,7 +83,20 @@ def ignore_command( # noqa: C901 bool, typer.Option('--global', '-g', help='Add an ignore rule to the global CLI config.') ] = False, ) -> None: - """Ignores a specific value, path or rule ID.""" + """:no_entry: [bold cyan]Ignore specific findings or paths in scans.[/] + + This command allows you to exclude specific items from Cycode scans, including: + * Paths: Exclude specific files or directories + * Rules: Ignore specific security rules + * Values: Exclude specific sensitive values + * Packages: Ignore specific package versions + * CVEs: Exclude specific vulnerabilities + + Example usage: + * `cycode ignore --by-path .env`: Ignore the tests directory + * `cycode ignore --by-rule GUID`: Ignore rule with the specified GUID + * `cycode ignore --by-package lodash@4.17.21`: Ignore lodash version 4.17.21 + """ add_breadcrumb('ignore') all_by_values = [by_value, by_sha, by_path, by_rule, by_package, by_cve] @@ -145,4 +158,4 @@ def ignore_command( # noqa: C901 'exclusion_value': exclusion_value, }, ) - configuration_manager.add_exclusion(configuration_scope, scan_type, exclusion_type, exclusion_value) + configuration_manager.add_exclusion(configuration_scope, str(scan_type), exclusion_type, exclusion_value) diff --git a/cycode/cli/apps/report/report_command.py b/cycode/cli/apps/report/report_command.py index 91a061c3..75debb33 100644 --- a/cycode/cli/apps/report/report_command.py +++ b/cycode/cli/apps/report/report_command.py @@ -5,7 +5,11 @@ def report_command(ctx: typer.Context) -> int: - """Generate report.""" + """:bar_chart: [bold cyan]Generate security reports.[/] + + Example usage: + * `cycode report sbom`: Generate SBOM report + """ add_breadcrumb('report') ctx.obj['progress_bar'] = get_progress_bar(hidden=False, sections=SBOM_REPORT_PROGRESS_BAR_SECTIONS) return 1 diff --git a/cycode/cli/apps/scan/__init__.py b/cycode/cli/apps/scan/__init__.py index 136e7bef..ada2d105 100644 --- a/cycode/cli/apps/scan/__init__.py +++ b/cycode/cli/apps/scan/__init__.py @@ -9,14 +9,23 @@ app = typer.Typer(name='scan', no_args_is_help=True) +_scan_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#scan-command' +_scan_command_epilog = f"""[bold]Documentation[/] + + + +For more details and advanced usage, visit: [link={_scan_command_docs}]{_scan_command_docs}[/link] +""" + app.callback( short_help='Scan the content for Secrets, IaC, SCA, and SAST violations.', result_callback=scan_command_result_callback, + epilog=_scan_command_epilog, )(scan_command) app.command(name='path', short_help='Scan the files in the paths provided in the command.')(path_command) app.command(name='repository', short_help='Scan the Git repository included files.')(repository_command) -app.command(name='commit-history', short_help='Scan all the commits history in this git repository.')( +app.command(name='commit-history', short_help='Scan all the commits history in this Git repository.')( commit_history_command ) app.command( diff --git a/cycode/cli/apps/scan/commit_history/commit_history_command.py b/cycode/cli/apps/scan/commit_history/commit_history_command.py index f7992a92..9eab3e34 100644 --- a/cycode/cli/apps/scan/commit_history/commit_history_command.py +++ b/cycode/cli/apps/scan/commit_history/commit_history_command.py @@ -12,7 +12,7 @@ def commit_history_command( ctx: typer.Context, path: Annotated[ - Path, typer.Argument(exists=True, resolve_path=True, help='Path to git repository to scan', show_default=False) + Path, typer.Argument(exists=True, resolve_path=True, help='Path to Git repository to scan', show_default=False) ], commit_range: Annotated[ str, diff --git a/cycode/cli/apps/scan/repository/repository_command.py b/cycode/cli/apps/scan/repository/repository_command.py index a99cc2d1..16ad8611 100644 --- a/cycode/cli/apps/scan/repository/repository_command.py +++ b/cycode/cli/apps/scan/repository/repository_command.py @@ -21,7 +21,7 @@ def repository_command( ctx: typer.Context, path: Annotated[ - Path, typer.Argument(exists=True, resolve_path=True, help='Path to git repository to scan.', show_default=False) + Path, typer.Argument(exists=True, resolve_path=True, help='Path to Git repository to scan.', show_default=False) ], branch: Annotated[ Optional[str], typer.Option('--branch', '-b', help='Branch to scan.', show_default='default branch') diff --git a/cycode/cli/apps/scan/scan_command.py b/cycode/cli/apps/scan/scan_command.py index 84485c0b..59bdb1fa 100644 --- a/cycode/cli/apps/scan/scan_command.py +++ b/cycode/cli/apps/scan/scan_command.py @@ -99,9 +99,21 @@ def scan_command( ), ] = False, ) -> None: - """:magnifying_glass_tilted_right: Scan the content for Secrets, IaC, SCA, and SAST violations. - You'll need to specify which scan type to perform: - [cyan]path[/]/[cyan]repository[/]/[cyan]commit_history[/].""" + """:mag: [bold cyan]Scan code for vulnerabilities (Secrets, IaC, SCA, SAST).[/] + + + This command scans your code for various types of security issues, including: + * [yellow]Secrets:[/] Hardcoded credentials and sensitive information. + * [dodger_blue1]Infrastructure as Code (IaC):[/] Misconfigurations in Terraform, CloudFormation, etc. + * [green]Software Composition Analysis (SCA):[/] Vulnerabilities and license issues in dependencies. + * [magenta]Static Application Security Testing (SAST):[/] Code quality and security flaws. + + Example usage: + * `cycode scan path `: Scan a specific local directory or file. + * `cycode scan repository `: Scan Git related files in a local Git repository. + * `cycode scan commit-history `: Scan the commit history of a local Git repository. + + """ add_breadcrumb('scan') ctx.obj['show_secret'] = show_secret diff --git a/cycode/cli/apps/status/get_cli_status.py b/cycode/cli/apps/status/get_cli_status.py index 4a3dc4b0..0a272c57 100644 --- a/cycode/cli/apps/status/get_cli_status.py +++ b/cycode/cli/apps/status/get_cli_status.py @@ -1,4 +1,5 @@ import platform +from typing import TYPE_CHECKING from cycode import __version__ from cycode.cli.apps.auth.auth_common import get_authorization_info @@ -8,11 +9,14 @@ from cycode.cli.user_settings.configuration_manager import ConfigurationManager from cycode.cli.utils.get_api_client import get_scan_cycode_client +if TYPE_CHECKING: + from typer import Context -def get_cli_status() -> CliStatus: + +def get_cli_status(ctx: 'Context') -> CliStatus: configuration_manager = ConfigurationManager() - auth_info = get_authorization_info() + auth_info = get_authorization_info(ctx) is_authenticated = auth_info is not None supported_modules_status = CliSupportedModulesStatus() diff --git a/cycode/cli/apps/status/status_command.py b/cycode/cli/apps/status/status_command.py index 28f8cfba..4654ef20 100644 --- a/cycode/cli/apps/status/status_command.py +++ b/cycode/cli/apps/status/status_command.py @@ -6,9 +6,25 @@ def status_command(ctx: typer.Context) -> None: + """:information_source: [bold cyan]Show Cycode CLI status and configuration.[/] + + This command displays the current status and configuration of the Cycode CLI, including: + * Authentication status: Whether you're logged in + * Version information: Current CLI version + * Configuration: Current API endpoints and settings + * System information: Operating system and environment details + + Output formats: + * Text: Human-readable format (default) + * JSON: Machine-readable format + + Example usage: + * `cycode status`: Show status in text format + * `cycode -o json status`: Show status in JSON format + """ output = ctx.obj['output'] - cli_status = get_cli_status() + cli_status = get_cli_status(ctx) if output == OutputTypeOption.JSON: console.print_json(cli_status.as_json()) else: diff --git a/cycode/cli/exceptions/handle_scan_errors.py b/cycode/cli/exceptions/handle_scan_errors.py index 09890247..f4d320bf 100644 --- a/cycode/cli/exceptions/handle_scan_errors.py +++ b/cycode/cli/exceptions/handle_scan_errors.py @@ -38,7 +38,7 @@ def handle_scan_exception(ctx: typer.Context, err: Exception, *, return_exceptio git_proxy.get_invalid_git_repository_error(): CliError( soft_fail=False, code='invalid_git_error', - message='The path you supplied does not correlate to a git repository. ' + message='The path you supplied does not correlate to a Git repository. ' 'If you still wish to scan this path, use: `cycode scan path `', ), } diff --git a/cycode/cli/utils/git_proxy.py b/cycode/cli/utils/git_proxy.py index c46d016b..47e77fc1 100644 --- a/cycode/cli/utils/git_proxy.py +++ b/cycode/cli/utils/git_proxy.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Optional, Type _GIT_ERROR_MESSAGE = """ -Cycode CLI needs the git executable to be installed on the system. +Cycode CLI needs the Git executable to be installed on the system. Git executable must be available in the PATH. Git 1.7.x or newer is required. You can help Cycode CLI to locate the Git executable From e06d52046ee0977f559f2fc28f0859461e10c94f Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Mon, 28 Apr 2025 14:32:51 +0200 Subject: [PATCH 08/11] add epilog to main entrypoint --- cycode/cli/app.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cycode/cli/app.py b/cycode/cli/app.py index 507c03c8..b07b3221 100644 --- a/cycode/cli/app.py +++ b/cycode/cli/app.py @@ -25,10 +25,19 @@ rich_utils.RICH_HELP = "Try [cyan]'{command_path} {help_option}'[/] for help." +_cycode_cli_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md' +_cycode_cli_epilog = f"""[bold]Documentation[/] + + + +For more details and advanced usage, visit: [link={_cycode_cli_docs}]{_cycode_cli_docs}[/link] +""" + app = typer.Typer( pretty_exceptions_show_locals=False, pretty_exceptions_short=True, context_settings=CLI_CONTEXT_SETTINGS, + epilog=_cycode_cli_epilog, rich_markup_mode='rich', no_args_is_help=True, add_completion=False, # we add it manually to control the rich help panel @@ -125,6 +134,7 @@ def app_callback( ), ] = False, ) -> None: + """[bold cyan]Cycode CLI - Command Line Interface for Cycode.[/]""" init_sentry() add_breadcrumb('cycode') From d83878f09300dfe4b0fc5c358d540ce2d7f381a8 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Mon, 28 Apr 2025 16:16:50 +0200 Subject: [PATCH 09/11] update some docs; fix commit range option name --- .pre-commit-hooks.yaml | 4 ++-- README.md | 20 +++++++++---------- .../commit_history/commit_history_command.py | 4 ++-- pyproject.toml | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 40e7a614..02a86db0 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,10 +3,10 @@ language: python language_version: python3 entry: cycode - args: [ '--no-progress-meter', 'scan', '--scan-type', 'secret', 'pre_commit' ] + args: [ '--no-progress-meter', 'scan', '--scan-type', 'secret', 'pre-commit' ] - id: cycode-sca name: Cycode SCA pre-commit defender language: python language_version: python3 entry: cycode - args: [ '--no-progress-meter', 'scan', '--scan-type', 'sca', 'pre_commit' ] + args: [ '--no-progress-meter', 'scan', '--scan-type', 'sca', 'pre-commit' ] diff --git a/README.md b/README.md index 6218cba8..fbe5c6a6 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ The following are the options and commands available with the Cycode CLI applica | [auth](#using-the-auth-command) | Authenticate your machine to associate the CLI with your Cycode account. | | [configure](#using-the-configure-command) | Initial command to configure your CLI client authentication. | | [ignore](#ignoring-scan-results) | Ignores a specific value, path or rule ID. | -| [scan](#running-a-scan) | Scan the content for Secrets/IaC/SCA/SAST violations. You`ll need to specify which scan type to perform: commit_history/path/repository/etc. | +| [scan](#running-a-scan) | Scan the content for Secrets/IaC/SCA/SAST violations. You`ll need to specify which scan type to perform: commit-history/path/repository/etc. | | [report](#report-command) | Generate report. You`ll need to specify which report type to perform. | | status | Show the CLI status and exit. | @@ -294,7 +294,7 @@ The Cycode CLI application offers several types of scans so that you can choose | Option | Description | |------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-t, --scan-type [secret\|iac\|sca\|sast]` | Specify the scan you wish to execute (`secret`/`iac`/`sca`/`sast`), the default is `secret`. | -| `--secret TEXT` | Specify a Cycode client secret for this specific scan execution. | +| `--client-secret TEXT` | Specify a Cycode client secret for this specific scan execution. | | `--client-id TEXT` | Specify a Cycode client ID for this specific scan execution. | | `--show-secret BOOLEAN` | Show secrets in plain text. See [Show/Hide Secrets](#showhide-secrets) section for more details. | | `--soft-fail BOOLEAN` | Run scan without failing, always return a non-error status code. See [Soft Fail](#soft-fail) section for more details. | @@ -308,9 +308,9 @@ The Cycode CLI application offers several types of scans so that you can choose | Command | Description | |----------------------------------------|-----------------------------------------------------------------| -| [commit_history](#commit-history-scan) | Scan all the commits history in this git repository | +| [commit-history](#commit-history-scan) | Scan all the commits history in this git repository | | [path](#path-scan) | Scan the files in the path supplied in the command | -| [pre_commit](#pre-commit-scan) | Use this command to scan the content that was not committed yet | +| [pre-commit](#pre-commit-scan) | Use this command to scan the content that was not committed yet | | [repository](#repository-scan) | Scan git repository including its history | ### Options @@ -466,25 +466,25 @@ A commit history scan is limited to a local repository’s previous commits, foc To execute a commit history scan, execute the following: -`cycode scan commit_history {{path}}` +`cycode scan commit-history {{path}}` For example, consider a scenario in which you want to scan the commit history for a repository stored in `~/home/git/codebase`. You could then execute the following: -`cycode scan commit_history ~/home/git/codebase` +`cycode scan commit-history ~/home/git/codebase` The following options are available for use with this command: | Option | Description | |---------------------------|----------------------------------------------------------------------------------------------------------| -| `-r, --commit_range TEXT` | Scan a commit range in this git repository, by default cycode scans all commit history (example: HEAD~1) | +| `-r, --commit-range TEXT` | Scan a commit range in this git repository, by default cycode scans all commit history (example: HEAD~1) | #### Commit Range Option -The commit history scan, by default, examines the repository’s entire commit history, all the way back to the initial commit. You can instead limit the scan to a specific commit range by adding the argument `--commit_range` (`-r`) followed by the name you specify. +The commit history scan, by default, examines the repository’s entire commit history, all the way back to the initial commit. You can instead limit the scan to a specific commit range by adding the argument `--commit-range` (`-r`) followed by the name you specify. Consider the previous example. If you wanted to scan only specific commits in your repository, you could execute the following: -`cycode scan commit_history -r {{from-commit-id}}...{{to-commit-id}} ~/home/git/codebase` +`cycode scan commit-history -r {{from-commit-id}}...{{to-commit-id}} ~/home/git/codebase` ### Pre-Commit Scan @@ -823,7 +823,7 @@ The following commands are available for use with this command: | Command | Description | |------------------|-----------------------------------------------------------------| | `path` | Generate SBOM report for provided path in the command | -| `repository_url` | Generate SBOM report for provided repository URI in the command | +| `repository-url` | Generate SBOM report for provided repository URI in the command | ### Repository diff --git a/cycode/cli/apps/scan/commit_history/commit_history_command.py b/cycode/cli/apps/scan/commit_history/commit_history_command.py index 9eab3e34..fc1ef23f 100644 --- a/cycode/cli/apps/scan/commit_history/commit_history_command.py +++ b/cycode/cli/apps/scan/commit_history/commit_history_command.py @@ -17,9 +17,9 @@ def commit_history_command( commit_range: Annotated[ str, typer.Option( - '--commit_range', + '--commit-range', '-r', - help='Scan a commit range in this git repository (example: HEAD~1)', + help='Scan a commit range in this Git repository (example: HEAD~1)', show_default='cycode scans all commit history', ), ] = '--all', diff --git a/pyproject.toml b/pyproject.toml index cde794b7..496ad41c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ style = "pep440" [tool.ruff] line-length = 120 -target-version = "py38" +target-version = "py39" [tool.ruff.lint] extend-select = [ From 4ff812c1ad91717bfc7882ca9561709ce2ba3e10 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 29 Apr 2025 00:14:10 +0200 Subject: [PATCH 10/11] bump typer with good bugfixes --- cycode/cli/apps/scan/scan_command.py | 1 - poetry.lock | 339 +++++++++++++-------------- pyproject.toml | 2 +- 3 files changed, 165 insertions(+), 177 deletions(-) diff --git a/cycode/cli/apps/scan/scan_command.py b/cycode/cli/apps/scan/scan_command.py index 59bdb1fa..32d04085 100644 --- a/cycode/cli/apps/scan/scan_command.py +++ b/cycode/cli/apps/scan/scan_command.py @@ -101,7 +101,6 @@ def scan_command( ) -> None: """:mag: [bold cyan]Scan code for vulnerabilities (Secrets, IaC, SCA, SAST).[/] - This command scans your code for various types of security issues, including: * [yellow]Secrets:[/] Hardcoded credentials and sensitive information. * [dodger_blue1]Infrastructure as Code (IaC):[/] Misconfigurations in Terraform, CloudFormation, etc. diff --git a/poetry.lock b/poetry.lock index d0b6503d..12fdf576 100644 --- a/poetry.lock +++ b/poetry.lock @@ -50,14 +50,14 @@ chardet = ">=3.0.2" [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main", "test"] files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, ] [[package]] @@ -74,129 +74,116 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" groups = ["main", "test"] files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -320,14 +307,14 @@ test = ["pytest (>=6)"] [[package]] name = "gitdb" -version = "4.0.11" +version = "4.0.12" description = "Git Object Database" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, - {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, ] [package.dependencies] @@ -335,21 +322,21 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.43" +version = "3.1.44" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, - {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, + {file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"}, + {file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] @@ -369,15 +356,15 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.7.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["executable"] markers = "python_version < \"3.10\"" files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] @@ -389,19 +376,19 @@ cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["test"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -496,14 +483,14 @@ test = ["pytest (<5.4)", "pytest-cov"] [[package]] name = "packaging" -version = "24.2" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "executable", "test"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] @@ -548,26 +535,26 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pyfakefs" -version = "5.7.2" +version = "5.7.4" description = "pyfakefs implements a fake file system that mocks the Python file system modules." optional = false python-versions = ">=3.7" groups = ["test"] files = [ - {file = "pyfakefs-5.7.2-py3-none-any.whl", hash = "sha256:e1527b0e8e4b33be52f0b024ca1deb269c73eecd68457c6b0bf608d6dab12ebd"}, - {file = "pyfakefs-5.7.2.tar.gz", hash = "sha256:40da84175c5af8d9c4f3b31800b8edc4af1e74a212671dd658b21cc881c60000"}, + {file = "pyfakefs-5.7.4-py3-none-any.whl", hash = "sha256:3e763d700b91c54ade6388be2cfa4e521abc00e34f7defb84ee511c73031f45f"}, + {file = "pyfakefs-5.7.4.tar.gz", hash = "sha256:4971e65cc80a93a1e6f1e3a4654909c0c493186539084dc9301da3d68c8878fe"}, ] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -610,32 +597,32 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2024.10" +version = "2025.3" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.8" groups = ["executable"] markers = "python_version < \"3.13\"" files = [ - {file = "pyinstaller_hooks_contrib-2024.10-py3-none-any.whl", hash = "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10"}, - {file = "pyinstaller_hooks_contrib-2024.10.tar.gz", hash = "sha256:8a46655e5c5b0186b5e527399118a9b342f10513eb1425c483fa4f6d02e8800c"}, + {file = "pyinstaller_hooks_contrib-2025.3-py3-none-any.whl", hash = "sha256:70cba46b1a6b82ae9104f074c25926e31f3dde50ff217434d1d660355b949683"}, + {file = "pyinstaller_hooks_contrib-2025.3.tar.gz", hash = "sha256:af129da5cd6219669fbda360e295cc822abac55b7647d03fec63a8fcf0a608cf"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +importlib_metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} packaging = ">=22.0" setuptools = ">=42.0.0" [[package]] name = "pyjwt" -version = "2.9.0" +version = "2.10.1" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] [package.extras] @@ -869,14 +856,14 @@ files = [ [[package]] name = "sentry-sdk" -version = "2.19.2" +version = "2.27.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"}, - {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"}, + {file = "sentry_sdk-2.27.0-py2.py3-none-any.whl", hash = "sha256:c58935bfff8af6a0856d37e8adebdbc7b3281c2b632ec823ef03cd108d216ff0"}, + {file = "sentry_sdk-2.27.0.tar.gz", hash = "sha256:90f4f883f9eff294aff59af3d58c2d1b64e3927b28d5ada2b9b41f5aeda47daf"}, ] [package.dependencies] @@ -920,29 +907,31 @@ sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] starlette = ["starlette (>=0.19.1)"] starlite = ["starlite (>=1.48)"] +statsig = ["statsig (>=0.55.3)"] tornado = ["tornado (>=6)"] +unleash = ["UnleashClient (>=6.0.1)"] [[package]] name = "setuptools" -version = "75.3.0" +version = "80.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["executable"] markers = "python_version < \"3.13\"" files = [ - {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, - {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, + {file = "setuptools-80.0.0-py3-none-any.whl", hash = "sha256:a38f898dcd6e5380f4da4381a87ec90bd0a7eec23d204a5552e80ee3cab6bd27"}, + {file = "setuptools-80.0.0.tar.gz", hash = "sha256:c40a5b3729d58dd749c0f08f1a07d134fb8a0a3d7f87dc33e7c5e1f762138650"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.5.2) ; sys_platform != \"cygwin\""] -core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.12.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "shellingham" @@ -970,14 +959,14 @@ files = [ [[package]] name = "smmap" -version = "5.0.1" +version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, + {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, + {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] [[package]] @@ -1041,14 +1030,14 @@ files = [ [[package]] name = "typer" -version = "0.15.2" +version = "0.15.3" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"}, - {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"}, + {file = "typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd"}, + {file = "typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c"}, ] [package.dependencies] @@ -1071,26 +1060,26 @@ files = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20240917" +version = "6.0.12.20250402" description = "Typing stubs for PyYAML" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["test"] files = [ - {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, - {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, + {file = "types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681"}, + {file = "types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075"}, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] [[package]] @@ -1112,15 +1101,15 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["executable"] markers = "python_version < \"3.10\"" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] @@ -1134,4 +1123,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "590be7f6a392d52a8d298596ef95e6ee664a8a3515530b01d727fe268e15fb0d" +content-hash = "83dddf9e309d442909a29574532caab06f9b0534f54083bc97ce994e8031d018" diff --git a/pyproject.toml b/pyproject.toml index 496ad41c..994b0378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ sentry-sdk = ">=2.8.0,<3.0" pyjwt = ">=2.8.0,<3.0" rich = ">=13.9.4, <14" patch-ng = "1.18.1" -typer = "^0.15.2" +typer = "^0.15.3" tenacity = ">=9.0.0,<9.1.0" [tool.poetry.group.test.dependencies] From e5b2c085cc060709dfc63f191ee07b955cfdc8cb Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 29 Apr 2025 01:06:21 +0200 Subject: [PATCH 11/11] update ruff; enable more rules; bump codebase to py39 syntax --- CONTRIBUTING.md | 4 +- cycode/cli/apps/auth/auth_command.py | 1 - cycode/cli/apps/auth/auth_manager.py | 4 +- cycode/cli/apps/scan/code_scanner.py | 77 +++++++++---------- cycode/cli/apps/scan/path/path_command.py | 4 +- .../scan/pre_commit/pre_commit_command.py | 4 +- .../scan/pre_receive/pre_receive_command.py | 4 +- cycode/cli/apps/scan/scan_command.py | 8 +- cycode/cli/apps/status/models.py | 3 +- cycode/cli/config.py | 2 +- cycode/cli/consts.py | 2 +- cycode/cli/exceptions/custom_exceptions.py | 6 +- cycode/cli/exceptions/handle_scan_errors.py | 3 +- cycode/cli/files_collector/excluder.py | 8 +- .../iac/tf_content_generator.py | 7 +- .../files_collector/models/in_memory_zip.py | 2 +- cycode/cli/files_collector/path_documents.py | 18 ++--- .../files_collector/repository_documents.py | 9 ++- .../sca/base_restore_dependencies.py | 6 +- .../sca/go/restore_go_dependencies.py | 4 +- .../sca/maven/restore_gradle_dependencies.py | 10 +-- .../sca/maven/restore_maven_dependencies.py | 6 +- .../sca/npm/restore_npm_dependencies.py | 3 +- .../sca/nuget/restore_nuget_dependencies.py | 3 +- .../sca/ruby/restore_ruby_dependencies.py | 4 +- .../sca/sbt/restore_sbt_dependencies.py | 4 +- .../files_collector/sca/sca_code_scanner.py | 30 ++++---- cycode/cli/files_collector/walk_ignore.py | 6 +- cycode/cli/files_collector/zip_documents.py | 4 +- cycode/cli/models.py | 18 ++--- cycode/cli/printers/console_printer.py | 8 +- cycode/cli/printers/json_printer.py | 6 +- cycode/cli/printers/printer_base.py | 9 ++- cycode/cli/printers/rich_printer.py | 6 +- .../cli/printers/tables/sca_table_printer.py | 8 +- cycode/cli/printers/tables/table.py | 16 ++-- cycode/cli/printers/tables/table_models.py | 6 +- cycode/cli/printers/tables/table_printer.py | 4 +- .../cli/printers/tables/table_printer_base.py | 8 +- cycode/cli/printers/text_printer.py | 8 +- .../detection_ordering/common_ordering.py | 20 ++--- .../utils/detection_ordering/sca_ordering.py | 11 +-- cycode/cli/user_settings/base_file_manager.py | 7 +- .../cli/user_settings/config_file_manager.py | 11 +-- .../user_settings/configuration_manager.py | 10 +-- .../cli/user_settings/credentials_manager.py | 10 +-- cycode/cli/utils/enum_utils.py | 3 +- cycode/cli/utils/get_api_client.py | 4 +- cycode/cli/utils/git_proxy.py | 18 ++--- cycode/cli/utils/ignore_utils.py | 30 ++++---- cycode/cli/utils/jwt_utils.py | 4 +- cycode/cli/utils/path_utils.py | 12 +-- cycode/cli/utils/progress_bar.py | 10 +-- cycode/cli/utils/scan_batch.py | 16 ++-- cycode/cli/utils/shell_executor.py | 4 +- cycode/cli/utils/task_timer.py | 12 ++- cycode/cli/utils/version_checker.py | 16 ++-- cycode/cli/utils/yaml_utils.py | 15 ++-- cycode/cyclient/cycode_client_base.py | 12 +-- cycode/cyclient/cycode_dev_based_client.py | 4 +- cycode/cyclient/headers.py | 1 + cycode/cyclient/models.py | 52 ++++++------- cycode/cyclient/report_client.py | 4 +- cycode/cyclient/scan_client.py | 10 +-- cycode/logger.py | 4 +- poetry.lock | 40 +++++----- process_executable_file.py | 10 +-- pyproject.toml | 16 +++- .../commands/version/test_version_checker.py | 2 +- .../cli/exceptions/test_handle_scan_errors.py | 4 +- tests/cli/files_collector/test_walk_ignore.py | 4 +- tests/cyclient/test_auth_client.py | 4 +- tests/cyclient/test_scan_client.py | 11 ++- tests/test_performance_get_all_files.py | 14 ++-- .../test_configuration_manager.py | 3 +- tests/utils/test_ignore_utils.py | 6 +- 76 files changed, 385 insertions(+), 372 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75b8e85f..857a27cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ But it’s fine to use a higher version without using new features from these ve The project is under Poetry project management. To deal with it, you should install it on your system: -Install Poetry (feel free to use Brew, etc): +Install Poetry (feel free to use Brew, etc.): ```shell curl -sSL https://install.python-poetry.org | python - -y @@ -70,6 +70,8 @@ poetry run ruff format . Many rules support auto-fixing. You can run it with the `--fix` flag. +Plugin for JB IDEs with auto formatting on save is available [here](https://plugins.jetbrains.com/plugin/20574-ruff). + ### Branching and versioning We use the `main` branch as the main one. diff --git a/cycode/cli/apps/auth/auth_command.py b/cycode/cli/apps/auth/auth_command.py index bf577ec8..817e0213 100644 --- a/cycode/cli/apps/auth/auth_command.py +++ b/cycode/cli/apps/auth/auth_command.py @@ -16,7 +16,6 @@ def auth_command(ctx: typer.Context) -> None: * `cycode auth`: Start interactive authentication * `cycode auth --help`: View authentication options """ - add_breadcrumb('auth') printer = ctx.obj.get('console_printer') diff --git a/cycode/cli/apps/auth/auth_manager.py b/cycode/cli/apps/auth/auth_manager.py index 2652bfe1..56a480e4 100644 --- a/cycode/cli/apps/auth/auth_manager.py +++ b/cycode/cli/apps/auth/auth_manager.py @@ -1,6 +1,6 @@ import time import webbrowser -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING from cycode.cli.exceptions.custom_exceptions import AuthProcessError from cycode.cli.user_settings.configuration_manager import ConfigurationManager @@ -78,7 +78,7 @@ def get_api_token_polling(self, session_id: str, code_verifier: str) -> 'ApiToke def save_api_token(self, api_token: 'ApiToken') -> None: self.credentials_manager.update_credentials(api_token.client_id, api_token.secret) - def _generate_pkce_code_pair(self) -> Tuple[str, str]: + def _generate_pkce_code_pair(self) -> tuple[str, str]: code_verifier = generate_random_string(self.CODE_VERIFIER_LENGTH) code_challenge = hash_string_to_sha256(code_verifier) return code_challenge, code_verifier diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index 1723647a..0209d9da 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -3,7 +3,7 @@ import sys import time from platform import platform -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Callable, Optional from uuid import UUID, uuid4 import click @@ -84,7 +84,7 @@ def scan_sca_commit_range(ctx: typer.Context, path: str, commit_range: str) -> N scan_commit_range_documents(ctx, from_commit_documents, to_commit_documents, scan_parameters=scan_parameters) -def scan_disk_files(ctx: typer.Context, paths: Tuple[str]) -> None: +def scan_disk_files(ctx: typer.Context, paths: tuple[str, ...]) -> None: scan_type = ctx.obj['scan_type'] progress_bar = ctx.obj['progress_bar'] @@ -96,7 +96,7 @@ def scan_disk_files(ctx: typer.Context, paths: Tuple[str]) -> None: handle_scan_exception(ctx, e) -def set_issue_detected_by_scan_results(ctx: typer.Context, scan_results: List[LocalScanResult]) -> None: +def set_issue_detected_by_scan_results(ctx: typer.Context, scan_results: list[LocalScanResult]) -> None: set_issue_detected(ctx, any(scan_result.issue_detected for scan_result in scan_results)) @@ -110,6 +110,7 @@ def _should_use_sync_flow(command_scan_type: str, scan_type: str, sync_option: b - for IAC scan, sync flow is always used - for SAST scan, sync flow is not supported - for SCA and Secrets scan, sync flow is supported only for path/repository scan + """ if not sync_option and scan_type != consts.IAC_SCAN_TYPE: return False @@ -161,14 +162,14 @@ def _enrich_scan_result_with_data_from_detection_rules( def _get_scan_documents_thread_func( ctx: typer.Context, is_git_diff: bool, is_commit_range: bool, scan_parameters: dict -) -> Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]]: +) -> Callable[[list[Document]], tuple[str, CliError, LocalScanResult]]: cycode_client = ctx.obj['client'] scan_type = ctx.obj['scan_type'] severity_threshold = ctx.obj['severity_threshold'] sync_option = ctx.obj['sync'] command_scan_type = ctx.info_name - def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, LocalScanResult]: + def _scan_batch_thread_func(batch: list[Document]) -> tuple[str, CliError, LocalScanResult]: local_scan_result = error = error_message = None detections_count = relevant_detections_count = zip_file_size = 0 @@ -297,7 +298,7 @@ def scan_commit_range( def scan_documents( ctx: typer.Context, - documents_to_scan: List[Document], + documents_to_scan: list[Document], scan_parameters: dict, is_git_diff: bool = False, is_commit_range: bool = False, @@ -335,13 +336,12 @@ def scan_documents( def scan_commit_range_documents( ctx: typer.Context, - from_documents_to_scan: List[Document], - to_documents_to_scan: List[Document], + from_documents_to_scan: list[Document], + to_documents_to_scan: list[Document], scan_parameters: Optional[dict] = None, timeout: Optional[int] = None, ) -> None: - """Used by SCA only""" - + """In use by SCA only.""" cycode_client = ctx.obj['client'] scan_type = ctx.obj['scan_type'] severity_threshold = ctx.obj['severity_threshold'] @@ -424,13 +424,13 @@ def scan_commit_range_documents( ) -def should_scan_documents(from_documents_to_scan: List[Document], to_documents_to_scan: List[Document]) -> bool: +def should_scan_documents(from_documents_to_scan: list[Document], to_documents_to_scan: list[Document]) -> bool: return len(from_documents_to_scan) > 0 or len(to_documents_to_scan) > 0 def create_local_scan_result( scan_result: ZippedFileScanResult, - documents_to_scan: List[Document], + documents_to_scan: list[Document], command_scan_type: str, scan_type: str, severity_threshold: str, @@ -568,15 +568,15 @@ def print_debug_scan_details(scan_details_response: 'ScanDetailsResponse') -> No def print_results( - ctx: typer.Context, local_scan_results: List[LocalScanResult], errors: Optional[Dict[str, 'CliError']] = None + ctx: typer.Context, local_scan_results: list[LocalScanResult], errors: Optional[dict[str, 'CliError']] = None ) -> None: printer = ctx.obj.get('console_printer') printer.print_scan_results(local_scan_results, errors) def get_document_detections( - scan_result: ZippedFileScanResult, documents_to_scan: List[Document] -) -> List[DocumentDetections]: + scan_result: ZippedFileScanResult, documents_to_scan: list[Document] +) -> list[DocumentDetections]: logger.debug('Getting document detections') document_detections = [] @@ -595,11 +595,11 @@ def get_document_detections( def exclude_irrelevant_document_detections( - document_detections_list: List[DocumentDetections], + document_detections_list: list[DocumentDetections], scan_type: str, command_scan_type: str, severity_threshold: str, -) -> List[DocumentDetections]: +) -> list[DocumentDetections]: relevant_document_detections_list = [] for document_detections in document_detections_list: relevant_detections = exclude_irrelevant_detections( @@ -614,8 +614,7 @@ def exclude_irrelevant_document_detections( def parse_pre_receive_input() -> str: - """ - Parsing input to pushed branch update details + """Parse input to pushed branch update details. Example input: old_value new_value refname @@ -624,7 +623,7 @@ def parse_pre_receive_input() -> str: 973a96d3e925b65941f7c47fa16129f1577d499f 0000000000000000000000000000000000000000 refs/heads/feature-branch 59564ef68745bca38c42fc57a7822efd519a6bd9 3378e52dcfa47fb11ce3a4a520bea5f85d5d0bf3 refs/heads/develop - :return: first branch update details (input's first line) + :return: First branch update details (input's first line) """ # FIXME(MarshalX): this blocks main thread forever if called outside of pre-receive hook pre_receive_input = sys.stdin.read().strip() @@ -649,7 +648,7 @@ def _get_default_scan_parameters(ctx: typer.Context) -> dict: } -def get_scan_parameters(ctx: typer.Context, paths: Optional[Tuple[str]] = None) -> dict: +def get_scan_parameters(ctx: typer.Context, paths: Optional[tuple[str, ...]] = None) -> dict: scan_parameters = _get_default_scan_parameters(ctx) if not paths: @@ -684,7 +683,7 @@ def try_get_git_remote_url(path: str) -> Optional[str]: def _get_plastic_repository_name(path: str) -> Optional[str]: - """Gets the name of the Plastic repository from the current working directory. + """Get the name of the Plastic repository from the current working directory. The command to execute is: cm status --header --machinereadable --fieldseparator=":::" @@ -692,7 +691,6 @@ def _get_plastic_repository_name(path: str) -> Optional[str]: Example of status header in machine-readable format: STATUS:::0:::Project/RepoName:::OrgName@ServerInfo """ - try: command = [ 'cm', @@ -718,8 +716,8 @@ def _get_plastic_repository_name(path: str) -> Optional[str]: return None -def _get_plastic_repository_list(working_dir: Optional[str] = None) -> Dict[str, str]: - """Gets the list of Plastic repositories and their GUIDs. +def _get_plastic_repository_list(working_dir: Optional[str] = None) -> dict[str, str]: + """Get the list of Plastic repositories and their GUIDs. The command to execute is: cm repo list --format="{repname}:::{repguid}" @@ -729,7 +727,6 @@ def _get_plastic_repository_list(working_dir: Optional[str] = None) -> Dict[str, Each line represents an individual repository. """ - repo_name_to_guid = {} try: @@ -771,14 +768,14 @@ def try_to_get_plastic_remote_url(path: str) -> Optional[str]: def exclude_irrelevant_detections( - detections: List[Detection], scan_type: str, command_scan_type: str, severity_threshold: str -) -> List[Detection]: + detections: list[Detection], scan_type: str, command_scan_type: str, severity_threshold: str +) -> list[Detection]: relevant_detections = _exclude_detections_by_exclusions_configuration(detections, scan_type) relevant_detections = _exclude_detections_by_scan_type(relevant_detections, scan_type, command_scan_type) return _exclude_detections_by_severity(relevant_detections, severity_threshold) -def _exclude_detections_by_severity(detections: List[Detection], severity_threshold: str) -> List[Detection]: +def _exclude_detections_by_severity(detections: list[Detection], severity_threshold: str) -> list[Detection]: relevant_detections = [] for detection in detections: severity = detection.severity @@ -795,8 +792,8 @@ def _exclude_detections_by_severity(detections: List[Detection], severity_thresh def _exclude_detections_by_scan_type( - detections: List[Detection], scan_type: str, command_scan_type: str -) -> List[Detection]: + detections: list[Detection], scan_type: str, command_scan_type: str +) -> list[Detection]: if command_scan_type == consts.PRE_COMMIT_COMMAND_SCAN_TYPE: return exclude_detections_in_deleted_lines(detections) @@ -811,16 +808,16 @@ def _exclude_detections_by_scan_type( return detections -def exclude_detections_in_deleted_lines(detections: List[Detection]) -> List[Detection]: +def exclude_detections_in_deleted_lines(detections: list[Detection]) -> list[Detection]: return [detection for detection in detections if detection.detection_details.get('line_type') != 'Removed'] -def _exclude_detections_by_exclusions_configuration(detections: List[Detection], scan_type: str) -> List[Detection]: +def _exclude_detections_by_exclusions_configuration(detections: list[Detection], scan_type: str) -> list[Detection]: exclusions = configuration_manager.get_exclusions_by_scan_type(scan_type) return [detection for detection in detections if not _should_exclude_detection(detection, exclusions)] -def _should_exclude_detection(detection: Detection, exclusions: Dict) -> bool: +def _should_exclude_detection(detection: Detection, exclusions: dict) -> bool: # FIXME(MarshalX): what the difference between by_value and by_sha? exclusions_by_value = exclusions.get(consts.EXCLUSIONS_BY_VALUE_SECTION_NAME, []) if _is_detection_sha_configured_in_exclusions(detection, exclusions_by_value): @@ -862,7 +859,7 @@ def _should_exclude_detection(detection: Detection, exclusions: Dict) -> bool: return False -def _is_detection_sha_configured_in_exclusions(detection: Detection, exclusions: List[str]) -> bool: +def _is_detection_sha_configured_in_exclusions(detection: Detection, exclusions: list[str]) -> bool: detection_sha = detection.detection_details.get('sha512') return detection_sha in exclusions @@ -886,7 +883,7 @@ def _get_cve_identifier(detection: Detection) -> Optional[str]: def _get_document_by_file_name( - documents: List[Document], file_name: str, unique_id: Optional[str] = None + documents: list[Document], file_name: str, unique_id: Optional[str] = None ) -> Optional[Document]: for document in documents: if _normalize_file_path(document.path) == _normalize_file_path(file_name) and document.unique_id == unique_id: @@ -992,10 +989,11 @@ def _try_get_aggregation_report_url_if_needed( logger.debug('Failed to get aggregation report url: %s', str(e)) -def _map_detections_per_file_and_commit_id(scan_type: str, raw_detections: List[dict]) -> List[DetectionsPerFile]: - """Converts list of detections (async flow) to list of DetectionsPerFile objects (sync flow). +def _map_detections_per_file_and_commit_id(scan_type: str, raw_detections: list[dict]) -> list[DetectionsPerFile]: + """Convert a list of detections (async flow) to list of DetectionsPerFile objects (sync flow). Args: + scan_type: Type of the scan. raw_detections: List of detections as is returned from the server. Note: @@ -1004,6 +1002,7 @@ def _map_detections_per_file_and_commit_id(scan_type: str, raw_detections: List[ Note: Aggregation is performed by file name and commit ID (if available) + """ detections_per_files = {} for raw_detection in raw_detections: @@ -1045,7 +1044,7 @@ def _get_secret_file_name_from_detection(raw_detection: dict) -> str: return os.path.join(file_path, file_name) -def _does_reach_to_max_commits_to_scan_limit(commit_ids: List[str], max_commits_count: Optional[int]) -> bool: +def _does_reach_to_max_commits_to_scan_limit(commit_ids: list[str], max_commits_count: Optional[int]) -> bool: if max_commits_count is None: return False diff --git a/cycode/cli/apps/scan/path/path_command.py b/cycode/cli/apps/scan/path/path_command.py index 48db40ac..3ee87350 100644 --- a/cycode/cli/apps/scan/path/path_command.py +++ b/cycode/cli/apps/scan/path/path_command.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Annotated, List +from typing import Annotated import typer @@ -11,7 +11,7 @@ def path_command( ctx: typer.Context, paths: Annotated[ - List[Path], typer.Argument(exists=True, resolve_path=True, help='Paths to scan', show_default=False) + list[Path], typer.Argument(exists=True, resolve_path=True, help='Paths to scan', show_default=False) ], ) -> None: add_breadcrumb('path') diff --git a/cycode/cli/apps/scan/pre_commit/pre_commit_command.py b/cycode/cli/apps/scan/pre_commit/pre_commit_command.py index 8e528d15..b919d659 100644 --- a/cycode/cli/apps/scan/pre_commit/pre_commit_command.py +++ b/cycode/cli/apps/scan/pre_commit/pre_commit_command.py @@ -1,5 +1,5 @@ import os -from typing import Annotated, List, Optional +from typing import Annotated, Optional import typer @@ -21,7 +21,7 @@ def pre_commit_command( ctx: typer.Context, - _: Annotated[Optional[List[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None, + _: Annotated[Optional[list[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None, ) -> None: add_breadcrumb('pre_commit') diff --git a/cycode/cli/apps/scan/pre_receive/pre_receive_command.py b/cycode/cli/apps/scan/pre_receive/pre_receive_command.py index 01242b24..eb4f1420 100644 --- a/cycode/cli/apps/scan/pre_receive/pre_receive_command.py +++ b/cycode/cli/apps/scan/pre_receive/pre_receive_command.py @@ -1,5 +1,5 @@ import os -from typing import Annotated, List, Optional +from typing import Annotated, Optional import click import typer @@ -25,7 +25,7 @@ def pre_receive_command( ctx: typer.Context, - _: Annotated[Optional[List[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None, + _: Annotated[Optional[list[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None, ) -> None: try: add_breadcrumb('pre_receive') diff --git a/cycode/cli/apps/scan/scan_command.py b/cycode/cli/apps/scan/scan_command.py index 32d04085..38e4a610 100644 --- a/cycode/cli/apps/scan/scan_command.py +++ b/cycode/cli/apps/scan/scan_command.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Optional +from typing import Annotated, Optional import click import typer @@ -67,7 +67,7 @@ def scan_command( ), ] = False, sca_scan: Annotated[ - List[ScaScanTypeOption], + list[ScaScanTypeOption], typer.Option( help='Specify the type of SCA scan you wish to execute.', rich_help_panel=_SCA_RICH_HELP_PANEL, @@ -85,7 +85,7 @@ def scan_command( bool, typer.Option( '--no-restore', - help='When specified, Cycode will not run restore command. ' 'Will scan direct dependencies [b]only[/]!', + help='When specified, Cycode will not run restore command. Will scan direct dependencies [b]only[/]!', rich_help_panel=_SCA_RICH_HELP_PANEL, ), ] = False, @@ -129,7 +129,7 @@ def scan_command( _sca_scan_to_context(ctx, sca_scan) -def _sca_scan_to_context(ctx: typer.Context, sca_scan_user_selected: List[str]) -> None: +def _sca_scan_to_context(ctx: typer.Context, sca_scan_user_selected: list[str]) -> None: for sca_scan_option_selected in sca_scan_user_selected: ctx.obj[sca_scan_option_selected] = True diff --git a/cycode/cli/apps/status/models.py b/cycode/cli/apps/status/models.py index 50182ecd..82b9751a 100644 --- a/cycode/cli/apps/status/models.py +++ b/cycode/cli/apps/status/models.py @@ -1,10 +1,9 @@ import json from dataclasses import asdict, dataclass -from typing import Dict class CliStatusBase: - def as_dict(self) -> Dict[str, any]: + def as_dict(self) -> dict[str, any]: return asdict(self) def _get_text_message_part(self, key: str, value: any, intent: int = 0) -> str: diff --git a/cycode/cli/config.py b/cycode/cli/config.py index a1ddbbaf..73491546 100644 --- a/cycode/cli/config.py +++ b/cycode/cli/config.py @@ -4,4 +4,4 @@ # env vars CYCODE_CLIENT_ID_ENV_VAR_NAME = 'CYCODE_CLIENT_ID' -CYCODE_CLIENT_SECRET_ENV_VAR_NAME = 'CYCODE_CLIENT_SECRET' # noqa: S105 +CYCODE_CLIENT_SECRET_ENV_VAR_NAME = 'CYCODE_CLIENT_SECRET' diff --git a/cycode/cli/consts.py b/cycode/cli/consts.py index 61297db2..286f1f95 100644 --- a/cycode/cli/consts.py +++ b/cycode/cli/consts.py @@ -9,7 +9,7 @@ COMMIT_HISTORY_COMMAND_SCAN_TYPE = 'commit-history' COMMIT_HISTORY_COMMAND_SCAN_TYPE_OLD = 'commit_history' -SECRET_SCAN_TYPE = 'secret' # noqa: S105 +SECRET_SCAN_TYPE = 'secret' IAC_SCAN_TYPE = 'iac' SCA_SCAN_TYPE = 'sca' SAST_SCAN_TYPE = 'sast' diff --git a/cycode/cli/exceptions/custom_exceptions.py b/cycode/cli/exceptions/custom_exceptions.py index 4d692812..59c0f693 100644 --- a/cycode/cli/exceptions/custom_exceptions.py +++ b/cycode/cli/exceptions/custom_exceptions.py @@ -4,7 +4,7 @@ class CycodeError(Exception): - """Base class for all custom exceptions""" + """Base class for all custom exceptions.""" def __str__(self) -> str: class_name = self.__class__.__name__ @@ -14,7 +14,7 @@ def __str__(self) -> str: class RequestError(CycodeError): ... -class RequestTimeout(RequestError): ... +class RequestTimeoutError(RequestError): ... class RequestConnectionError(RequestError): ... @@ -91,7 +91,7 @@ def __str__(self) -> str: code='cycode_error', message='Cycode was unable to complete this scan. Please try again by executing the `cycode scan` command', ), - RequestTimeout: CliError( + RequestTimeoutError: CliError( soft_fail=True, code='timeout_error', message='The request timed out. Please try again by executing the `cycode scan` command', diff --git a/cycode/cli/exceptions/handle_scan_errors.py b/cycode/cli/exceptions/handle_scan_errors.py index f4d320bf..229e0f02 100644 --- a/cycode/cli/exceptions/handle_scan_errors.py +++ b/cycode/cli/exceptions/handle_scan_errors.py @@ -17,8 +17,7 @@ def handle_scan_exception(ctx: typer.Context, err: Exception, *, return_exceptio custom_exceptions.ScanAsyncError: CliError( soft_fail=True, code='scan_error', - message='Cycode was unable to complete this scan. ' - 'Please try again by executing the `cycode scan` command', + message='Cycode was unable to complete this scan. Please try again by executing the `cycode scan` command', ), custom_exceptions.ZipTooLargeError: CliError( soft_fail=True, diff --git a/cycode/cli/files_collector/excluder.py b/cycode/cli/files_collector/excluder.py index f16e9710..9ef5e3d6 100644 --- a/cycode/cli/files_collector/excluder.py +++ b/cycode/cli/files_collector/excluder.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from cycode.cli import consts from cycode.cli.config import configuration_manager @@ -16,8 +16,8 @@ def exclude_irrelevant_files( - progress_bar: 'BaseProgressBar', progress_bar_section: 'ProgressBarSection', scan_type: str, filenames: List[str] -) -> List[str]: + progress_bar: 'BaseProgressBar', progress_bar_section: 'ProgressBarSection', scan_type: str, filenames: list[str] +) -> list[str]: relevant_files = [] for filename in filenames: progress_bar.update(progress_bar_section) @@ -29,7 +29,7 @@ def exclude_irrelevant_files( return relevant_files -def exclude_irrelevant_documents_to_scan(scan_type: str, documents_to_scan: List['Document']) -> List['Document']: +def exclude_irrelevant_documents_to_scan(scan_type: str, documents_to_scan: list['Document']) -> list['Document']: logger.debug('Excluding irrelevant documents to scan') relevant_documents = [] diff --git a/cycode/cli/files_collector/iac/tf_content_generator.py b/cycode/cli/files_collector/iac/tf_content_generator.py index 8f4cb4d0..63be9e47 100644 --- a/cycode/cli/files_collector/iac/tf_content_generator.py +++ b/cycode/cli/files_collector/iac/tf_content_generator.py @@ -1,6 +1,5 @@ import json import time -from typing import List from cycode.cli import consts from cycode.cli.exceptions.custom_exceptions import TfplanKeyError @@ -34,7 +33,7 @@ def generate_tf_content_from_tfplan(filename: str, tfplan: str) -> str: return _generate_tf_content(planned_resources) -def _generate_tf_content(resource_changes: List[ResourceChange]) -> str: +def _generate_tf_content(resource_changes: list[ResourceChange]) -> str: tf_content = '' for resource_change in resource_changes: if not any(item in resource_change.actions for item in ACTIONS_TO_OMIT_RESOURCE): @@ -62,9 +61,9 @@ def _get_resource_name(resource_change: ResourceChange) -> str: return '.'.join(valid_parts) -def _extract_resources(tfplan: str, filename: str) -> List[ResourceChange]: +def _extract_resources(tfplan: str, filename: str) -> list[ResourceChange]: tfplan_json = load_json(tfplan) - resources: List[ResourceChange] = [] + resources: list[ResourceChange] = [] try: resource_changes = tfplan_json['resource_changes'] for resource_change in resource_changes: diff --git a/cycode/cli/files_collector/models/in_memory_zip.py b/cycode/cli/files_collector/models/in_memory_zip.py index a0700f6b..8f58b12b 100644 --- a/cycode/cli/files_collector/models/in_memory_zip.py +++ b/cycode/cli/files_collector/models/in_memory_zip.py @@ -10,7 +10,7 @@ from pathlib import Path -class InMemoryZip(object): +class InMemoryZip: def __init__(self) -> None: self.configuration_manager = ConfigurationManager() diff --git a/cycode/cli/files_collector/path_documents.py b/cycode/cli/files_collector/path_documents.py index 469e6ce7..e0f06312 100644 --- a/cycode/cli/files_collector/path_documents.py +++ b/cycode/cli/files_collector/path_documents.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING from cycode.cli.files_collector.excluder import exclude_irrelevant_files from cycode.cli.files_collector.iac.tf_content_generator import ( @@ -17,8 +17,8 @@ from cycode.cli.utils.progress_bar import BaseProgressBar, ProgressBarSection -def _get_all_existing_files_in_directory(path: str, *, walk_with_ignore_patterns: bool = True) -> List[str]: - files: List[str] = [] +def _get_all_existing_files_in_directory(path: str, *, walk_with_ignore_patterns: bool = True) -> list[str]: + files: list[str] = [] walk_func = walk_ignore if walk_with_ignore_patterns else os.walk for root, _, filenames in walk_func(path): @@ -28,7 +28,7 @@ def _get_all_existing_files_in_directory(path: str, *, walk_with_ignore_patterns return files -def _get_relevant_files_in_path(path: str) -> List[str]: +def _get_relevant_files_in_path(path: str) -> list[str]: absolute_path = get_absolute_path(path) if not os.path.isfile(absolute_path) and not os.path.isdir(absolute_path): @@ -42,8 +42,8 @@ def _get_relevant_files_in_path(path: str) -> List[str]: def _get_relevant_files( - progress_bar: 'BaseProgressBar', progress_bar_section: 'ProgressBarSection', scan_type: str, paths: Tuple[str, ...] -) -> List[str]: + progress_bar: 'BaseProgressBar', progress_bar_section: 'ProgressBarSection', scan_type: str, paths: tuple[str, ...] +) -> list[str]: all_files_to_scan = [] for path in paths: all_files_to_scan.extend(_get_relevant_files_in_path(path)) @@ -89,13 +89,13 @@ def get_relevant_documents( progress_bar: 'BaseProgressBar', progress_bar_section: 'ProgressBarSection', scan_type: str, - paths: Tuple[str, ...], + paths: tuple[str, ...], *, is_git_diff: bool = False, -) -> List[Document]: +) -> list[Document]: relevant_files = _get_relevant_files(progress_bar, progress_bar_section, scan_type, paths) - documents: List[Document] = [] + documents: list[Document] = [] for file in relevant_files: progress_bar.update(progress_bar_section) diff --git a/cycode/cli/files_collector/repository_documents.py b/cycode/cli/files_collector/repository_documents.py index df49aa95..b524ca4c 100644 --- a/cycode/cli/files_collector/repository_documents.py +++ b/cycode/cli/files_collector/repository_documents.py @@ -1,5 +1,6 @@ import os -from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, Union +from collections.abc import Iterator +from typing import TYPE_CHECKING, Optional, Union from cycode.cli import consts from cycode.cli.files_collector.sca import sca_code_scanner @@ -25,7 +26,7 @@ def get_git_repository_tree_file_entries( return git_proxy.get_repo(path).tree(branch).traverse(predicate=should_process_git_object) -def parse_commit_range(commit_range: str, path: str) -> Tuple[str, str]: +def parse_commit_range(commit_range: str, path: str) -> tuple[str, str]: from_commit_rev = None to_commit_rev = None @@ -47,7 +48,7 @@ def get_diff_file_content(file: 'Diff') -> str: def get_pre_commit_modified_documents( progress_bar: 'BaseProgressBar', progress_bar_section: 'ProgressBarSection' -) -> Tuple[List[Document], List[Document]]: +) -> tuple[list[Document], list[Document]]: git_head_documents = [] pre_committed_documents = [] @@ -77,7 +78,7 @@ def get_commit_range_modified_documents( path: str, from_commit_rev: str, to_commit_rev: str, -) -> Tuple[List[Document], List[Document]]: +) -> tuple[list[Document], list[Document]]: from_commit_documents = [] to_commit_documents = [] diff --git a/cycode/cli/files_collector/sca/base_restore_dependencies.py b/cycode/cli/files_collector/sca/base_restore_dependencies.py index 2e6c0993..c4364c05 100644 --- a/cycode/cli/files_collector/sca/base_restore_dependencies.py +++ b/cycode/cli/files_collector/sca/base_restore_dependencies.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import List, Optional +from typing import Optional import typer @@ -14,7 +14,7 @@ def build_dep_tree_path(path: str, generated_file_name: str) -> str: def execute_commands( - commands: List[List[str]], + commands: list[list[str]], file_name: str, command_timeout: int, dependencies_file_name: Optional[str] = None, @@ -91,7 +91,7 @@ def is_project(self, document: Document) -> bool: pass @abstractmethod - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: pass @abstractmethod diff --git a/cycode/cli/files_collector/sca/go/restore_go_dependencies.py b/cycode/cli/files_collector/sca/go/restore_go_dependencies.py index 5d56644a..4f469896 100644 --- a/cycode/cli/files_collector/sca/go/restore_go_dependencies.py +++ b/cycode/cli/files_collector/sca/go/restore_go_dependencies.py @@ -1,5 +1,5 @@ import os -from typing import List, Optional +from typing import Optional import typer @@ -34,7 +34,7 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]: def is_project(self, document: Document) -> bool: return any(document.path.endswith(ext) for ext in GO_PROJECT_FILE_EXTENSIONS) - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return [ ['go', 'list', '-m', '-json', 'all'], ['echo', '------------------------------------------------------'], diff --git a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py index 3995da90..89595e0e 100644 --- a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py @@ -1,6 +1,6 @@ import os import re -from typing import List, Optional, Set +from typing import Optional import typer @@ -19,7 +19,7 @@ class RestoreGradleDependencies(BaseRestoreDependencies): def __init__( - self, ctx: typer.Context, is_git_diff: bool, command_timeout: int, projects: Optional[Set[str]] = None + self, ctx: typer.Context, is_git_diff: bool, command_timeout: int, projects: Optional[set[str]] = None ) -> None: super().__init__(ctx, is_git_diff, command_timeout, create_output_file_manually=True) if projects is None: @@ -32,7 +32,7 @@ def is_gradle_sub_projects(self) -> bool: def is_project(self, document: Document) -> bool: return document.path.endswith(BUILD_GRADLE_FILE_NAME) or document.path.endswith(BUILD_GRADLE_KTS_FILE_NAME) - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return ( self.get_commands_for_sub_projects(manifest_file_path) if self.is_gradle_sub_projects() @@ -48,7 +48,7 @@ def verify_restore_file_already_exist(self, restore_file_path: str) -> bool: def get_working_directory(self, document: Document) -> Optional[str]: return get_path_from_context(self.ctx) if self.is_gradle_sub_projects() else None - def get_all_projects(self) -> Set[str]: + def get_all_projects(self) -> set[str]: projects_output = shell( command=BUILD_GRADLE_ALL_PROJECTS_COMMAND, timeout=BUILD_GRADLE_ALL_PROJECTS_TIMEOUT, @@ -59,7 +59,7 @@ def get_all_projects(self) -> Set[str]: return set(projects) - def get_commands_for_sub_projects(self, manifest_file_path: str) -> List[List[str]]: + def get_commands_for_sub_projects(self, manifest_file_path: str) -> list[list[str]]: project_name = os.path.basename(os.path.dirname(manifest_file_path)) project_name = f':{project_name}' return ( diff --git a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py index d90bbe71..1c3d860c 100644 --- a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py @@ -1,6 +1,6 @@ import os from os import path -from typing import List, Optional +from typing import Optional import typer @@ -24,7 +24,7 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) def is_project(self, document: Document) -> bool: return path.basename(document.path).split('/')[-1] == BUILD_MAVEN_FILE_NAME - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return [['mvn', 'org.cyclonedx:cyclonedx-maven-plugin:2.7.4:makeAggregateBom', '-f', manifest_file_path]] def get_lock_file_name(self) -> str: @@ -64,7 +64,7 @@ def restore_from_secondary_command( return restore_dependencies -def create_secondary_restore_command(manifest_file_path: str) -> List[str]: +def create_secondary_restore_command(manifest_file_path: str) -> list[str]: return [ 'mvn', 'dependency:tree', diff --git a/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py b/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py index 672ee0db..ed8e36c2 100644 --- a/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +++ b/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py @@ -1,5 +1,4 @@ import os -from typing import List import typer @@ -18,7 +17,7 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) def is_project(self, document: Document) -> bool: return any(document.path.endswith(ext) for ext in NPM_PROJECT_FILE_EXTENSIONS) - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return [ [ 'npm', diff --git a/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py b/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py index b4f5a248..3bd6627f 100644 --- a/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py +++ b/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py @@ -1,5 +1,4 @@ import os -from typing import List import typer @@ -17,7 +16,7 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) def is_project(self, document: Document) -> bool: return any(document.path.endswith(ext) for ext in NUGET_PROJECT_FILE_EXTENSIONS) - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return [['dotnet', 'restore', manifest_file_path, '--use-lock-file', '--verbosity', 'quiet']] def get_lock_file_name(self) -> str: diff --git a/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py b/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py index 3dfc4a16..4571b1c5 100644 --- a/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py +++ b/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py @@ -1,5 +1,5 @@ import os -from typing import List, Optional +from typing import Optional from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies from cycode.cli.models import Document @@ -12,7 +12,7 @@ class RestoreRubyDependencies(BaseRestoreDependencies): def is_project(self, document: Document) -> bool: return any(document.path.endswith(ext) for ext in RUBY_PROJECT_FILE_EXTENSIONS) - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return [['bundle', '--quiet']] def get_lock_file_name(self) -> str: diff --git a/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py b/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py index b8e1c41b..d7eeba3b 100644 --- a/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py +++ b/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py @@ -1,5 +1,5 @@ import os -from typing import List, Optional +from typing import Optional from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies from cycode.cli.models import Document @@ -12,7 +12,7 @@ class RestoreSbtDependencies(BaseRestoreDependencies): def is_project(self, document: Document) -> bool: return any(document.path.endswith(ext) for ext in SBT_PROJECT_FILE_EXTENSIONS) - def get_commands(self, manifest_file_path: str) -> List[List[str]]: + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return [['sbt', 'dependencyLockWrite', '--verbose']] def get_lock_file_name(self) -> str: diff --git a/cycode/cli/files_collector/sca/sca_code_scanner.py b/cycode/cli/files_collector/sca/sca_code_scanner.py index 88626c9c..e6ec0e9d 100644 --- a/cycode/cli/files_collector/sca/sca_code_scanner.py +++ b/cycode/cli/files_collector/sca/sca_code_scanner.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional import typer @@ -28,9 +28,9 @@ def perform_pre_commit_range_scan_actions( path: str, - from_commit_documents: List[Document], + from_commit_documents: list[Document], from_commit_rev: str, - to_commit_documents: List[Document], + to_commit_documents: list[Document], to_commit_rev: str, ) -> None: repo = git_proxy.get_repo(path) @@ -39,7 +39,7 @@ def perform_pre_commit_range_scan_actions( def perform_pre_hook_range_scan_actions( - git_head_documents: List[Document], pre_committed_documents: List[Document] + git_head_documents: list[Document], pre_committed_documents: list[Document] ) -> None: repo = git_proxy.get_repo(os.getcwd()) add_ecosystem_related_files_if_exists(git_head_documents, repo, consts.GIT_HEAD_COMMIT_REV) @@ -47,9 +47,9 @@ def perform_pre_hook_range_scan_actions( def add_ecosystem_related_files_if_exists( - documents: List[Document], repo: Optional['Repo'] = None, commit_rev: Optional[str] = None + documents: list[Document], repo: Optional['Repo'] = None, commit_rev: Optional[str] = None ) -> None: - documents_to_add: List[Document] = [] + documents_to_add: list[Document] = [] for doc in documents: ecosystem = get_project_file_ecosystem(doc) if ecosystem is None: @@ -62,9 +62,9 @@ def add_ecosystem_related_files_if_exists( def get_doc_ecosystem_related_project_files( - doc: Document, documents: List[Document], ecosystem: str, commit_rev: Optional[str], repo: Optional['Repo'] -) -> List[Document]: - documents_to_add: List[Document] = [] + doc: Document, documents: list[Document], ecosystem: str, commit_rev: Optional[str], repo: Optional['Repo'] +) -> list[Document]: + documents_to_add: list[Document] = [] for ecosystem_project_file in consts.PROJECT_FILES_BY_ECOSYSTEM_MAP.get(ecosystem): file_to_search = join_paths(get_file_dir(doc.path), ecosystem_project_file) if not is_project_file_exists_in_documents(documents, file_to_search): @@ -79,7 +79,7 @@ def get_doc_ecosystem_related_project_files( return documents_to_add -def is_project_file_exists_in_documents(documents: List[Document], file: str) -> bool: +def is_project_file_exists_in_documents(documents: list[Document], file: str) -> bool: return any(doc for doc in documents if file == doc.path) @@ -93,7 +93,7 @@ def get_project_file_ecosystem(document: Document) -> Optional[str]: def try_restore_dependencies( ctx: typer.Context, - documents_to_add: Dict[str, Document], + documents_to_add: dict[str, Document], restore_dependencies: 'BaseRestoreDependencies', document: Document, ) -> None: @@ -122,9 +122,9 @@ def try_restore_dependencies( def add_dependencies_tree_document( - ctx: typer.Context, documents_to_scan: List[Document], is_git_diff: bool = False + ctx: typer.Context, documents_to_scan: list[Document], is_git_diff: bool = False ) -> None: - documents_to_add: Dict[str, Document] = {document.path: document for document in documents_to_scan} + documents_to_add: dict[str, Document] = {document.path: document for document in documents_to_scan} restore_dependencies_list = restore_handlers(ctx, is_git_diff) for restore_dependencies in restore_dependencies_list: @@ -135,7 +135,7 @@ def add_dependencies_tree_document( documents_to_scan[:] = list(documents_to_add.values()) -def restore_handlers(ctx: typer.Context, is_git_diff: bool) -> List[BaseRestoreDependencies]: +def restore_handlers(ctx: typer.Context, is_git_diff: bool) -> list[BaseRestoreDependencies]: return [ RestoreGradleDependencies(ctx, is_git_diff, BUILD_DEP_TREE_TIMEOUT), RestoreMavenDependencies(ctx, is_git_diff, BUILD_DEP_TREE_TIMEOUT), @@ -159,7 +159,7 @@ def get_file_content_from_commit(repo: 'Repo', commit: str, file_path: str) -> O def perform_pre_scan_documents_actions( - ctx: typer.Context, scan_type: str, documents_to_scan: List[Document], is_git_diff: bool = False + ctx: typer.Context, scan_type: str, documents_to_scan: list[Document], is_git_diff: bool = False ) -> None: no_restore = ctx.params.get('no-restore', False) if scan_type == consts.SCA_SCAN_TYPE and not no_restore: diff --git a/cycode/cli/files_collector/walk_ignore.py b/cycode/cli/files_collector/walk_ignore.py index 0ba2b93d..35855ff4 100644 --- a/cycode/cli/files_collector/walk_ignore.py +++ b/cycode/cli/files_collector/walk_ignore.py @@ -1,5 +1,5 @@ import os -from typing import Generator, Iterable, List, Tuple +from collections.abc import Generator, Iterable from cycode.cli.logger import logger from cycode.cli.utils.ignore_utils import IgnoreFilterManager @@ -22,7 +22,7 @@ def _walk_to_top(path: str) -> Iterable[str]: yield path # Include the top-level directory -def _collect_top_level_ignore_files(path: str) -> List[str]: +def _collect_top_level_ignore_files(path: str) -> list[str]: ignore_files = [] top_paths = reversed(list(_walk_to_top(path))) # we must reverse it to make top levels more prioritized for dir_path in top_paths: @@ -34,7 +34,7 @@ def _collect_top_level_ignore_files(path: str) -> List[str]: return ignore_files -def walk_ignore(path: str) -> Generator[Tuple[str, List[str], List[str]], None, None]: +def walk_ignore(path: str) -> Generator[tuple[str, list[str], list[str]], None, None]: ignore_filter_manager = IgnoreFilterManager.build( path=path, global_ignore_file_paths=_collect_top_level_ignore_files(path), diff --git a/cycode/cli/files_collector/zip_documents.py b/cycode/cli/files_collector/zip_documents.py index b9a272e1..770121fa 100644 --- a/cycode/cli/files_collector/zip_documents.py +++ b/cycode/cli/files_collector/zip_documents.py @@ -1,6 +1,6 @@ import timeit from pathlib import Path -from typing import List, Optional +from typing import Optional from cycode.cli import consts from cycode.cli.exceptions import custom_exceptions @@ -17,7 +17,7 @@ def _validate_zip_file_size(scan_type: str, zip_file_size: int) -> None: raise custom_exceptions.ZipTooLargeError(max_size_limit) -def zip_documents(scan_type: str, documents: List[Document], zip_file: Optional[InMemoryZip] = None) -> InMemoryZip: +def zip_documents(scan_type: str, documents: list[Document], zip_file: Optional[InMemoryZip] = None) -> InMemoryZip: if zip_file is None: zip_file = InMemoryZip() diff --git a/cycode/cli/models.py b/cycode/cli/models.py index 14058f0c..3c59eeee 100644 --- a/cycode/cli/models.py +++ b/cycode/cli/models.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Dict, List, NamedTuple, Optional, Type +from typing import NamedTuple, Optional from cycode.cyclient.models import Detection @@ -20,16 +20,16 @@ def __init__( self.absolute_path = absolute_path def __repr__(self) -> str: - return 'path:{0}, content:{1}'.format(self.path, self.content) + return f'path:{self.path}, content:{self.content}' class DocumentDetections: - def __init__(self, document: Document, detections: List[Detection]) -> None: + def __init__(self, document: Document, detections: list[Detection]) -> None: self.document = document self.detections = detections def __repr__(self) -> str: - return 'document:{0}, detections:{1}'.format(self.document, self.detections) + return f'document:{self.document}, detections:{self.detections}' class CliError(NamedTuple): @@ -42,19 +42,19 @@ def enrich(self, additional_message: str) -> 'CliError': return CliError(self.code, message, self.soft_fail) -CliErrors = Dict[Type[BaseException], CliError] +CliErrors = dict[type[BaseException], CliError] class CliResult(NamedTuple): success: bool message: str - data: Optional[Dict[str, any]] = None + data: Optional[dict[str, any]] = None class LocalScanResult(NamedTuple): scan_id: str report_url: Optional[str] - document_detections: List[DocumentDetections] + document_detections: list[DocumentDetections] issue_detected: bool detections_count: int relevant_detections_count: int @@ -66,8 +66,8 @@ class ResourceChange: resource_type: str name: str index: Optional[int] - actions: List[str] - values: Dict[str, str] + actions: list[str] + values: dict[str, str] def __repr__(self) -> str: return f'resource_type: {self.resource_type}, name: {self.name}' diff --git a/cycode/cli/printers/console_printer.py b/cycode/cli/printers/console_printer.py index 00eb38cf..f581c894 100644 --- a/cycode/cli/printers/console_printer.py +++ b/cycode/cli/printers/console_printer.py @@ -1,5 +1,5 @@ import io -from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional, Type +from typing import TYPE_CHECKING, ClassVar, Optional import typer from rich.console import Console @@ -21,7 +21,7 @@ class ConsolePrinter: - _AVAILABLE_PRINTERS: ClassVar[Dict[str, Type['PrinterBase']]] = { + _AVAILABLE_PRINTERS: ClassVar[dict[str, type['PrinterBase']]] = { 'rich': RichPrinter, 'text': TextPrinter, 'json': JsonPrinter, @@ -78,8 +78,8 @@ def printer(self) -> 'PrinterBase': def print_scan_results( self, - local_scan_results: List['LocalScanResult'], - errors: Optional[Dict[str, 'CliError']] = None, + local_scan_results: list['LocalScanResult'], + errors: Optional[dict[str, 'CliError']] = None, ) -> None: if self.console_record: self.console_record.print_scan_results(local_scan_results, errors) diff --git a/cycode/cli/printers/json_printer.py b/cycode/cli/printers/json_printer.py index 6ad14e22..acb7912f 100644 --- a/cycode/cli/printers/json_printer.py +++ b/cycode/cli/printers/json_printer.py @@ -1,5 +1,5 @@ import json -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional from cycode.cli.models import CliError, CliResult from cycode.cli.printers.printer_base import PrinterBase @@ -21,7 +21,7 @@ def print_error(self, error: CliError) -> None: self.console.print_json(self.get_data_json(result)) def print_scan_results( - self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None ) -> None: scan_ids = [] report_urls = [] @@ -48,7 +48,7 @@ def print_scan_results( self.console.print_json(self._get_json_scan_result(scan_ids, detections_dict, report_urls, inlined_errors)) def _get_json_scan_result( - self, scan_ids: List[str], detections: dict, report_urls: List[str], errors: List[dict] + self, scan_ids: list[str], detections: dict, report_urls: list[str], errors: list[dict] ) -> str: result = { 'scan_ids': scan_ids, diff --git a/cycode/cli/printers/printer_base.py b/cycode/cli/printers/printer_base.py index 23ba7384..527cc31b 100644 --- a/cycode/cli/printers/printer_base.py +++ b/cycode/cli/printers/printer_base.py @@ -1,7 +1,7 @@ import sys from abc import ABC, abstractmethod from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional import typer @@ -51,7 +51,7 @@ def show_secret(self) -> bool: @abstractmethod def print_scan_results( - self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None ) -> None: pass @@ -68,6 +68,7 @@ def print_exception(self, e: Optional[BaseException] = None) -> None: Note: Called only when the verbose flag is set. + """ rich_traceback = ( RichTraceback.from_exception(type(e), e, e.__traceback__) @@ -79,7 +80,7 @@ def print_exception(self, e: Optional[BaseException] = None) -> None: self.console_err.print(f'[red]Correlation ID:[/] {get_correlation_id()}') - def print_scan_results_summary(self, local_scan_results: List['LocalScanResult']) -> None: + def print_scan_results_summary(self, local_scan_results: list['LocalScanResult']) -> None: """Print a summary of scan results based on severity levels. Args: @@ -87,8 +88,8 @@ def print_scan_results_summary(self, local_scan_results: List['LocalScanResult'] The summary includes the count of detections for each severity level and is displayed in the console in a formatted string. - """ + """ detections_count = 0 severity_counts = defaultdict(int) for local_scan_result in local_scan_results: diff --git a/cycode/cli/printers/rich_printer.py b/cycode/cli/printers/rich_printer.py index 3401b8f5..b2ed1a2e 100644 --- a/cycode/cli/printers/rich_printer.py +++ b/cycode/cli/printers/rich_printer.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional from rich.console import Group from rich.panel import Panel @@ -25,7 +25,7 @@ class RichPrinter(TextPrinter): MAX_PATH_LENGTH = 60 def print_scan_results( - self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): self.console.print(self.NO_DETECTIONS_MESSAGE) @@ -57,7 +57,7 @@ def _get_details_table(self, detection: 'Detection') -> Table: detection_details = detection.detection_details path = str(get_detection_file_path(self.scan_type, detection)) - shorten_path = f'...{path[-self.MAX_PATH_LENGTH:]}' if len(path) > self.MAX_PATH_LENGTH else path + shorten_path = f'...{path[-self.MAX_PATH_LENGTH :]}' if len(path) > self.MAX_PATH_LENGTH else path details_table.add_row('In file', f'[link=file://{path}]{shorten_path}[/]') if self.scan_type == consts.SECRET_SCAN_TYPE: diff --git a/cycode/cli/printers/tables/sca_table_printer.py b/cycode/cli/printers/tables/sca_table_printer.py index 7606ae68..0bf59a20 100644 --- a/cycode/cli/printers/tables/sca_table_printer.py +++ b/cycode/cli/printers/tables/sca_table_printer.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING from cycode.cli.cli_types import SeverityOption from cycode.cli.consts import LICENSE_COMPLIANCE_POLICY_ID, PACKAGE_VULNERABILITY_POLICY_ID @@ -30,7 +30,7 @@ class ScaTablePrinter(TablePrinterBase): - def _print_results(self, local_scan_results: List['LocalScanResult']) -> None: + def _print_results(self, local_scan_results: list['LocalScanResult']) -> None: aggregation_report_url = self.ctx.obj.get('aggregation_report_url') detections_per_policy_id = self._extract_detections_per_policy_id(local_scan_results) for policy_id, detections in detections_per_policy_id.items(): @@ -128,8 +128,8 @@ def _print_summary_issues(self, detections_count: int, title: str) -> None: @staticmethod def _extract_detections_per_policy_id( - local_scan_results: List['LocalScanResult'], - ) -> Dict[str, List[Detection]]: + local_scan_results: list['LocalScanResult'], + ) -> dict[str, list[Detection]]: detections_to_policy_id = defaultdict(list) for local_scan_result in local_scan_results: diff --git a/cycode/cli/printers/tables/table.py b/cycode/cli/printers/tables/table.py index b89df4af..61e143ca 100644 --- a/cycode/cli/printers/tables/table.py +++ b/cycode/cli/printers/tables/table.py @@ -1,5 +1,5 @@ import urllib.parse -from typing import TYPE_CHECKING, Dict, List, Optional, Set +from typing import TYPE_CHECKING, Optional from rich.markup import escape from rich.table import Table as RichTable @@ -11,10 +11,10 @@ class Table: """Helper class to manage columns and their values in the right order and only if the column should be presented.""" - def __init__(self, column_infos: Optional[List['ColumnInfo']] = None) -> None: - self._group_separator_indexes: Set[int] = set() + def __init__(self, column_infos: Optional[list['ColumnInfo']] = None) -> None: + self._group_separator_indexes: set[int] = set() - self._columns: Dict['ColumnInfo', List[str]] = {} + self._columns: dict[ColumnInfo, list[str]] = {} if column_infos: self._columns = {columns: [] for columns in column_infos} @@ -37,17 +37,17 @@ def add_file_path_cell(self, column: 'ColumnInfo', path: str) -> None: escaped_path = escape(encoded_path) self._add_cell_no_error(column, f'[link file://{escaped_path}]{path}') - def set_group_separator_indexes(self, group_separator_indexes: Set[int]) -> None: + def set_group_separator_indexes(self, group_separator_indexes: set[int]) -> None: self._group_separator_indexes = group_separator_indexes - def _get_ordered_columns(self) -> List['ColumnInfo']: + def _get_ordered_columns(self) -> list['ColumnInfo']: # we are sorting columns by index to make sure that columns will be printed in the right order return sorted(self._columns, key=lambda column_info: column_info.index) - def get_columns_info(self) -> List['ColumnInfo']: + def get_columns_info(self) -> list['ColumnInfo']: return self._get_ordered_columns() - def get_rows(self) -> List[str]: + def get_rows(self) -> list[str]: column_values = [self._columns[column_info] for column_info in self._get_ordered_columns()] return list(zip(*column_values)) diff --git a/cycode/cli/printers/tables/table_models.py b/cycode/cli/printers/tables/table_models.py index 42e3b1fb..58e41aaa 100644 --- a/cycode/cli/printers/tables/table_models.py +++ b/cycode/cli/printers/tables/table_models.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, NamedTuple, Optional +from typing import NamedTuple, Optional class ColumnInfoBuilder: @@ -14,12 +14,12 @@ def build(self, name: str, **column_opts) -> 'ColumnInfo': class ColumnInfo(NamedTuple): name: str index: int # Represents the order of the columns, starting from the left - column_opts: Optional[Dict] = None + column_opts: Optional[dict] = None def __hash__(self) -> int: return hash((self.name, self.index)) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, ColumnInfo): return NotImplemented return (self.name, self.index) == (other.name, other.index) diff --git a/cycode/cli/printers/tables/table_printer.py b/cycode/cli/printers/tables/table_printer.py index 4f821c7f..fe9f8dd5 100644 --- a/cycode/cli/printers/tables/table_printer.py +++ b/cycode/cli/printers/tables/table_printer.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from cycode.cli.cli_types import SeverityOption from cycode.cli.consts import SECRET_SCAN_TYPE @@ -27,7 +27,7 @@ class TablePrinter(TablePrinterBase): - def _print_results(self, local_scan_results: List['LocalScanResult']) -> None: + def _print_results(self, local_scan_results: list['LocalScanResult']) -> None: table = self._get_table() detections, group_separator_indexes = sort_and_group_detections_from_scan_result(local_scan_results) diff --git a/cycode/cli/printers/tables/table_printer_base.py b/cycode/cli/printers/tables/table_printer_base.py index 5d2aaa73..d7a2b502 100644 --- a/cycode/cli/printers/tables/table_printer_base.py +++ b/cycode/cli/printers/tables/table_printer_base.py @@ -1,5 +1,5 @@ import abc -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional from cycode.cli.models import CliError, CliResult from cycode.cli.printers.printer_base import PrinterBase @@ -18,7 +18,7 @@ def print_error(self, error: CliError) -> None: TextPrinter(self.ctx, self.console, self.console_err).print_error(error) def print_scan_results( - self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): self.console.print(self.NO_DETECTIONS_MESSAGE) @@ -38,7 +38,7 @@ def _is_git_repository(self) -> bool: return self.ctx.info_name in {'commit_history', 'pre_commit', 'pre_receive'} and 'remote_url' in self.ctx.obj @abc.abstractmethod - def _print_results(self, local_scan_results: List['LocalScanResult']) -> None: + def _print_results(self, local_scan_results: list['LocalScanResult']) -> None: raise NotImplementedError def _print_table(self, table: 'Table') -> None: @@ -47,7 +47,7 @@ def _print_table(self, table: 'Table') -> None: def _print_report_urls( self, - local_scan_results: List['LocalScanResult'], + local_scan_results: list['LocalScanResult'], aggregation_report_url: Optional[str] = None, ) -> None: report_urls = [scan_result.report_url for scan_result in local_scan_results if scan_result.report_url] diff --git a/cycode/cli/printers/text_printer.py b/cycode/cli/printers/text_printer.py index 6eb4b78b..564456ae 100644 --- a/cycode/cli/printers/text_printer.py +++ b/cycode/cli/printers/text_printer.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional from cycode.cli.cli_types import SeverityOption from cycode.cli.models import CliError, CliResult, Document @@ -30,7 +30,7 @@ def print_error(self, error: CliError) -> None: self.console.print(f'[red]Error: {error.message}[/]', highlight=False) def print_scan_results( - self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): self.console.print(self.NO_DETECTIONS_MESSAGE) @@ -82,7 +82,7 @@ def __print_detection_code_segment(self, detection: 'Detection', document: Docum ) def print_report_urls_and_errors( - self, local_scan_results: List['LocalScanResult'], errors: Optional[Dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None ) -> None: report_urls = [scan_result.report_url for scan_result in local_scan_results if scan_result.report_url] @@ -95,7 +95,7 @@ def print_report_urls_and_errors( self.console.print(f'- {scan_id}: ', end='') self.print_error(error) - def print_report_urls(self, report_urls: List[str], aggregation_report_url: Optional[str] = None) -> None: + def print_report_urls(self, report_urls: list[str], aggregation_report_url: Optional[str] = None) -> None: if not report_urls and not aggregation_report_url: return if aggregation_report_url: diff --git a/cycode/cli/printers/utils/detection_ordering/common_ordering.py b/cycode/cli/printers/utils/detection_ordering/common_ordering.py index c0f8dddd..c4b431ef 100644 --- a/cycode/cli/printers/utils/detection_ordering/common_ordering.py +++ b/cycode/cli/printers/utils/detection_ordering/common_ordering.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, Set, Tuple +from typing import TYPE_CHECKING from cycode.cli.cli_types import SeverityOption @@ -7,34 +7,34 @@ from cycode.cyclient.models import Detection -GroupedDetections = Tuple[List[Tuple['Detection', 'Document']], Set[int]] +GroupedDetections = tuple[list[tuple['Detection', 'Document']], set[int]] -def __severity_sort_key(detection_with_document: Tuple['Detection', 'Document']) -> int: +def __severity_sort_key(detection_with_document: tuple['Detection', 'Document']) -> int: detection, _ = detection_with_document severity = detection.severity if detection.severity else '' return SeverityOption.get_member_weight(severity) def _sort_detections_by_severity( - detections_with_documents: List[Tuple['Detection', 'Document']], -) -> List[Tuple['Detection', 'Document']]: + detections_with_documents: list[tuple['Detection', 'Document']], +) -> list[tuple['Detection', 'Document']]: return sorted(detections_with_documents, key=__severity_sort_key, reverse=True) -def __file_path_sort_key(detection_with_document: Tuple['Detection', 'Document']) -> str: +def __file_path_sort_key(detection_with_document: tuple['Detection', 'Document']) -> str: _, document = detection_with_document return document.path def _sort_detections_by_file_path( - detections_with_documents: List[Tuple['Detection', 'Document']], -) -> List[Tuple['Detection', 'Document']]: + detections_with_documents: list[tuple['Detection', 'Document']], +) -> list[tuple['Detection', 'Document']]: return sorted(detections_with_documents, key=__file_path_sort_key) def sort_and_group_detections( - detections_with_documents: List[Tuple['Detection', 'Document']], + detections_with_documents: list[tuple['Detection', 'Document']], ) -> GroupedDetections: """Sort detections by severity. We do not have grouping here (don't find the best one yet).""" group_separator_indexes = set() @@ -46,7 +46,7 @@ def sort_and_group_detections( return sorted_by_severity, group_separator_indexes -def sort_and_group_detections_from_scan_result(local_scan_results: List['LocalScanResult']) -> GroupedDetections: +def sort_and_group_detections_from_scan_result(local_scan_results: list['LocalScanResult']) -> GroupedDetections: detections_with_documents = [] for local_scan_result in local_scan_results: for document_detections in local_scan_result.document_detections: diff --git a/cycode/cli/printers/utils/detection_ordering/sca_ordering.py b/cycode/cli/printers/utils/detection_ordering/sca_ordering.py index 3886f2c7..a8be3430 100644 --- a/cycode/cli/printers/utils/detection_ordering/sca_ordering.py +++ b/cycode/cli/printers/utils/detection_ordering/sca_ordering.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Set, Tuple +from typing import TYPE_CHECKING from cycode.cli.cli_types import SeverityOption @@ -7,7 +7,7 @@ from cycode.cyclient.models import Detection -def __group_by(detections: List['Detection'], details_field_name: str) -> Dict[str, List['Detection']]: +def __group_by(detections: list['Detection'], details_field_name: str) -> dict[str, list['Detection']]: grouped = defaultdict(list) for detection in detections: grouped[detection.detection_details.get(details_field_name)].append(detection) @@ -19,7 +19,7 @@ def __severity_sort_key(detection: 'Detection') -> int: return SeverityOption.get_member_weight(severity) -def _sort_detections_by_severity(detections: List['Detection']) -> List['Detection']: +def _sort_detections_by_severity(detections: list['Detection']) -> list['Detection']: return sorted(detections, key=__severity_sort_key, reverse=True) @@ -27,11 +27,11 @@ def __package_sort_key(detection: 'Detection') -> int: return detection.detection_details.get('package_name') -def _sort_detections_by_package(detections: List['Detection']) -> List['Detection']: +def _sort_detections_by_package(detections: list['Detection']) -> list['Detection']: return sorted(detections, key=__package_sort_key) -def sort_and_group_detections(detections: List['Detection']) -> Tuple[List['Detection'], Set[int]]: +def sort_and_group_detections(detections: list['Detection']) -> tuple[list['Detection'], set[int]]: """Sort detections by severity and group by repository, code project and package name. Note: @@ -39,6 +39,7 @@ def sort_and_group_detections(detections: List['Detection']) -> Tuple[List['Dete Grouping by code projects also groups by ecosystem. Because manifest files are unique per ecosystem. + """ resulting_detections = [] group_separator_indexes = set() diff --git a/cycode/cli/user_settings/base_file_manager.py b/cycode/cli/user_settings/base_file_manager.py index 4eb15e2a..4f07f11c 100644 --- a/cycode/cli/user_settings/base_file_manager.py +++ b/cycode/cli/user_settings/base_file_manager.py @@ -1,6 +1,7 @@ import os from abc import ABC, abstractmethod -from typing import Any, Dict, Hashable +from collections.abc import Hashable +from typing import Any from cycode.cli.utils.yaml_utils import read_yaml_file, update_yaml_file @@ -9,10 +10,10 @@ class BaseFileManager(ABC): @abstractmethod def get_filename(self) -> str: ... - def read_file(self) -> Dict[Hashable, Any]: + def read_file(self) -> dict[Hashable, Any]: return read_yaml_file(self.get_filename()) - def write_content_to_file(self, content: Dict[Hashable, Any]) -> None: + def write_content_to_file(self, content: dict[Hashable, Any]) -> None: filename = self.get_filename() os.makedirs(os.path.dirname(filename), exist_ok=True) update_yaml_file(filename, content) diff --git a/cycode/cli/user_settings/config_file_manager.py b/cycode/cli/user_settings/config_file_manager.py index e4e5e6b1..5b029e39 100644 --- a/cycode/cli/user_settings/config_file_manager.py +++ b/cycode/cli/user_settings/config_file_manager.py @@ -1,5 +1,6 @@ import os -from typing import TYPE_CHECKING, Any, Dict, Hashable, List, Optional, Union +from collections.abc import Hashable +from typing import TYPE_CHECKING, Any, Optional, Union from cycode.cli.consts import CYCODE_CONFIGURATION_DIRECTORY from cycode.cli.user_settings.base_file_manager import BaseFileManager @@ -37,7 +38,7 @@ def get_app_url(self) -> Optional[Any]: def get_verbose_flag(self) -> Optional[Any]: return self._get_value_from_environment_section(self.VERBOSE_FIELD_NAME) - def get_exclusions_by_scan_type(self, scan_type: str) -> Dict[Hashable, Any]: + def get_exclusions_by_scan_type(self, scan_type: str) -> dict[Hashable, Any]: exclusions_section = self._get_section(self.EXCLUSIONS_SECTION_NAME) return exclusions_section.get(scan_type, {}) @@ -87,7 +88,7 @@ def get_filename(self) -> str: def get_config_file_route() -> str: return os.path.join(ConfigFileManager.CYCODE_HIDDEN_DIRECTORY, ConfigFileManager.FILE_NAME) - def _get_exclusions_by_exclusion_type(self, scan_type: str, exclusion_type: str) -> List[Any]: + def _get_exclusions_by_exclusion_type(self, scan_type: str, exclusion_type: str) -> list[Any]: scan_type_exclusions = self.get_exclusions_by_scan_type(scan_type) return scan_type_exclusions.get(exclusion_type, []) @@ -95,7 +96,7 @@ def _get_value_from_environment_section(self, field_name: str) -> Optional[Any]: environment_section = self._get_section(self.ENVIRONMENT_SECTION_NAME) return environment_section.get(field_name) - def _get_scan_configuration_by_scan_type(self, command_scan_type: str) -> Dict[Hashable, Any]: + def _get_scan_configuration_by_scan_type(self, command_scan_type: str) -> dict[Hashable, Any]: scan_section = self._get_section(self.SCAN_SECTION_NAME) return scan_section.get(command_scan_type, {}) @@ -103,6 +104,6 @@ def _get_value_from_command_scan_type_configuration(self, command_scan_type: str command_scan_type_configuration = self._get_scan_configuration_by_scan_type(command_scan_type) return command_scan_type_configuration.get(field_name) - def _get_section(self, section_name: str) -> Dict[Hashable, Any]: + def _get_section(self, section_name: str) -> dict[Hashable, Any]: file_content = self.read_file() return file_content.get(section_name, {}) diff --git a/cycode/cli/user_settings/configuration_manager.py b/cycode/cli/user_settings/configuration_manager.py index f8d67c42..3b83f1c9 100644 --- a/cycode/cli/user_settings/configuration_manager.py +++ b/cycode/cli/user_settings/configuration_manager.py @@ -1,7 +1,7 @@ import os -from functools import lru_cache +from functools import cache from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Optional from uuid import uuid4 from cycode.cli import consts @@ -69,8 +69,8 @@ def get_verbose_flag_from_environment_variables(self) -> bool: value = self._get_value_from_environment_variables(consts.VERBOSE_ENV_VAR_NAME, '') return value.lower() in ('true', '1') - @lru_cache(maxsize=None) # noqa: B019 - def get_exclusions_by_scan_type(self, scan_type: str) -> Dict: + @cache # noqa: B019 + def get_exclusions_by_scan_type(self, scan_type: str) -> dict: local_exclusions = self.local_config_file_manager.get_exclusions_by_scan_type(scan_type) global_exclusions = self.global_config_file_manager.get_exclusions_by_scan_type(scan_type) return self._merge_exclusions(local_exclusions, global_exclusions) @@ -80,7 +80,7 @@ def add_exclusion(self, scope: str, scan_type: str, exclusion_type: str, value: config_file_manager.add_exclusion(scan_type, exclusion_type, value) @staticmethod - def _merge_exclusions(local_exclusions: Dict, global_exclusions: Dict) -> Dict: + def _merge_exclusions(local_exclusions: dict, global_exclusions: dict) -> dict: keys = set(list(local_exclusions.keys()) + list(global_exclusions.keys())) return {key: local_exclusions.get(key, []) + global_exclusions.get(key, []) for key in keys} diff --git a/cycode/cli/user_settings/credentials_manager.py b/cycode/cli/user_settings/credentials_manager.py index 86a84ba6..7af43569 100644 --- a/cycode/cli/user_settings/credentials_manager.py +++ b/cycode/cli/user_settings/credentials_manager.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Optional, Tuple +from typing import Optional from cycode.cli.config import CYCODE_CLIENT_ID_ENV_VAR_NAME, CYCODE_CLIENT_SECRET_ENV_VAR_NAME from cycode.cli.user_settings.base_file_manager import BaseFileManager @@ -19,7 +19,7 @@ class CredentialsManager(BaseFileManager): ACCESS_TOKEN_EXPIRES_IN_FIELD_NAME: str = 'cycode_access_token_expires_in' ACCESS_TOKEN_CREATOR_FIELD_NAME: str = 'cycode_access_token_creator' - def get_credentials(self) -> Tuple[str, str]: + def get_credentials(self) -> tuple[str, str]: client_id, client_secret = self.get_credentials_from_environment_variables() if client_id is not None and client_secret is not None: return client_id, client_secret @@ -27,12 +27,12 @@ def get_credentials(self) -> Tuple[str, str]: return self.get_credentials_from_file() @staticmethod - def get_credentials_from_environment_variables() -> Tuple[str, str]: + def get_credentials_from_environment_variables() -> tuple[str, str]: client_id = os.getenv(CYCODE_CLIENT_ID_ENV_VAR_NAME) client_secret = os.getenv(CYCODE_CLIENT_SECRET_ENV_VAR_NAME) return client_id, client_secret - def get_credentials_from_file(self) -> Tuple[Optional[str], Optional[str]]: + def get_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]: file_content = self.read_file() client_id = file_content.get(self.CLIENT_ID_FIELD_NAME) client_secret = file_content.get(self.CLIENT_SECRET_FIELD_NAME) @@ -42,7 +42,7 @@ def update_credentials(self, client_id: str, client_secret: str) -> None: file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.CLIENT_SECRET_FIELD_NAME: client_secret} self.write_content_to_file(file_content_to_update) - def get_access_token(self) -> Tuple[Optional[str], Optional[float], Optional[JwtCreator]]: + def get_access_token(self) -> tuple[Optional[str], Optional[float], Optional[JwtCreator]]: file_content = self.read_file() access_token = file_content.get(self.ACCESS_TOKEN_FIELD_NAME) diff --git a/cycode/cli/utils/enum_utils.py b/cycode/cli/utils/enum_utils.py index 6ea9ef72..3280a5bb 100644 --- a/cycode/cli/utils/enum_utils.py +++ b/cycode/cli/utils/enum_utils.py @@ -1,8 +1,7 @@ from enum import Enum -from typing import List class AutoCountEnum(Enum): @staticmethod - def _generate_next_value_(name: str, start: int, count: int, last_values: List[int]) -> int: + def _generate_next_value_(name: str, start: int, count: int, last_values: list[int]) -> int: return count diff --git a/cycode/cli/utils/get_api_client.py b/cycode/cli/utils/get_api_client.py index 7bbfa2d9..91e8f0f7 100644 --- a/cycode/cli/utils/get_api_client.py +++ b/cycode/cli/utils/get_api_client.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, Tuple, Union +from typing import TYPE_CHECKING, Optional, Union import click @@ -35,6 +35,6 @@ def get_report_cycode_client( return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log) -def _get_configured_credentials() -> Tuple[str, str]: +def _get_configured_credentials() -> tuple[str, str]: credentials_manager = CredentialsManager() return credentials_manager.get_credentials() diff --git a/cycode/cli/utils/git_proxy.py b/cycode/cli/utils/git_proxy.py index 47e77fc1..beaafdd0 100644 --- a/cycode/cli/utils/git_proxy.py +++ b/cycode/cli/utils/git_proxy.py @@ -1,6 +1,6 @@ import types from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Optional, Type +from typing import TYPE_CHECKING, Optional _GIT_ERROR_MESSAGE = """ Cycode CLI needs the Git executable to be installed on the system. @@ -31,10 +31,10 @@ def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo' def get_null_tree(self) -> object: ... @abstractmethod - def get_invalid_git_repository_error(self) -> Type[BaseException]: ... + def get_invalid_git_repository_error(self) -> type[BaseException]: ... @abstractmethod - def get_git_command_error(self) -> Type[BaseException]: ... + def get_git_command_error(self) -> type[BaseException]: ... class _DummyGitProxy(_AbstractGitProxy): @@ -44,10 +44,10 @@ def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo' def get_null_tree(self) -> object: raise RuntimeError(_GIT_ERROR_MESSAGE) - def get_invalid_git_repository_error(self) -> Type[BaseException]: + def get_invalid_git_repository_error(self) -> type[BaseException]: return GitProxyError - def get_git_command_error(self) -> Type[BaseException]: + def get_git_command_error(self) -> type[BaseException]: return GitProxyError @@ -58,10 +58,10 @@ def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo' def get_null_tree(self) -> object: return git.NULL_TREE - def get_invalid_git_repository_error(self) -> Type[BaseException]: + def get_invalid_git_repository_error(self) -> type[BaseException]: return git.InvalidGitRepositoryError - def get_git_command_error(self) -> Type[BaseException]: + def get_git_command_error(self) -> type[BaseException]: return git.GitCommandError @@ -87,10 +87,10 @@ def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo' def get_null_tree(self) -> object: return self._git_proxy.get_null_tree() - def get_invalid_git_repository_error(self) -> Type[BaseException]: + def get_invalid_git_repository_error(self) -> type[BaseException]: return self._git_proxy.get_invalid_git_repository_error() - def get_git_command_error(self) -> Type[BaseException]: + def get_git_command_error(self) -> type[BaseException]: return self._git_proxy.get_git_command_error() diff --git a/cycode/cli/utils/ignore_utils.py b/cycode/cli/utils/ignore_utils.py index f44b6024..e8994e46 100644 --- a/cycode/cli/utils/ignore_utils.py +++ b/cycode/cli/utils/ignore_utils.py @@ -38,16 +38,12 @@ import contextlib import os.path import re +from collections.abc import Generator, Iterable from os import PathLike from typing import ( Any, BinaryIO, - Dict, - Generator, - Iterable, - List, Optional, - Tuple, Union, ) @@ -98,7 +94,6 @@ def translate(pat: bytes) -> bytes: Originally copied from fnmatch in Python 2.7, but modified for Dulwich to cope with features in Git ignore patterns. """ - res = b'(?ms)' if b'/' not in pat[:-1]: @@ -131,6 +126,7 @@ def read_ignore_patterns(f: BinaryIO) -> Iterable[bytes]: Args: f: File-like object to read from Returns: List of patterns + """ for line in f: line = line.rstrip(b'\r\n') @@ -160,6 +156,7 @@ def match_pattern(path: bytes, pattern: bytes, ignore_case: bool = False) -> boo ignore_case: Whether to do case-sensitive matching Returns: bool indicating whether the pattern matched + """ return Pattern(pattern, ignore_case).match(path) @@ -200,6 +197,7 @@ def match(self, path: bytes) -> bool: Args: path: Path to match (relative to ignore location) Returns: boolean + """ return bool(self._re.match(path)) @@ -219,7 +217,7 @@ def __init__( for pattern in patterns: self.append_pattern(pattern) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: d = { 'patterns': [str(p) for p in self._patterns], 'ignore_case': self._ignore_case, @@ -242,6 +240,7 @@ def find_matching(self, path: Union[bytes, str]) -> Iterable[Pattern]: path: Path to match Returns: Iterator over iterators + """ if not isinstance(path, bytes): path = os.fsencode(path) @@ -284,7 +283,7 @@ class IgnoreFilterManager: def __init__( self, path: str, - global_filters: List[IgnoreFilter], + global_filters: list[IgnoreFilter], ignore_file_name: Optional[str] = None, ignore_case: bool = False, ) -> None: @@ -303,7 +302,7 @@ def __init__( def __repr__(self) -> str: return f'{type(self).__name__}({self._top_path}, {self._global_filters!r}, {self._ignore_case!r})' - def to_dict(self, include_path_filters: bool = True) -> Dict[str, Any]: + def to_dict(self, include_path_filters: bool = True) -> dict[str, Any]: d = { 'path': self._top_path, 'global_filters': [f.to_dict() for f in self._global_filters], @@ -337,7 +336,7 @@ def _load_path(self, path: str) -> Optional[IgnoreFilter]: p = os.path.join(self._top_path, path, self._ignore_file_name) try: self._path_filters[path] = IgnoreFilter.from_path(p, self._ignore_case) - except IOError: + except OSError: self._path_filters[path] = None return self._path_filters[path] @@ -348,6 +347,7 @@ def _find_matching(self, path: str) -> Iterable[Pattern]: path: Path to check Returns: Iterator over Pattern instances + """ if os.path.isabs(path): raise ValueError(f'{path} is an absolute path') @@ -379,6 +379,7 @@ def is_ignored(self, path: str) -> Optional[bool]: True if the path matches an ignore pattern, False if the path is explicitly not ignored, or None if the file does not match any patterns. + """ if hasattr(path, '__fspath__'): path = path.__fspath__() @@ -387,10 +388,8 @@ def is_ignored(self, path: str) -> Optional[bool]: return matches[-1].is_exclude return None - def walk(self, **kwargs) -> Generator[Tuple[str, List[str], List[str]], None, None]: - """A wrapper for os.walk() without ignored files and subdirectories. - kwargs are passed to walk().""" - + def walk(self, **kwargs) -> Generator[tuple[str, list[str], list[str]], None, None]: + """Wrap os.walk() without ignored files and subdirectories and kwargs are passed to walk.""" for dirpath, dirnames, filenames in os.walk(self.path, topdown=True, **kwargs): rel_dirpath = '' if dirpath == self.path else os.path.relpath(dirpath, self.path) @@ -413,6 +412,7 @@ def build( ignore_case: bool = False, ) -> 'IgnoreFilterManager': """Create a IgnoreFilterManager from patterns and paths. + Args: path: The root path for ignore checks. global_ignore_file_paths: A list of file paths to load patterns from. @@ -421,8 +421,10 @@ def build( global_patterns: Global patterns to ignore. ignore_file_name: The per-directory ignore file name. ignore_case: Whether to ignore case in matching. + Returns: A `IgnoreFilterManager` object + """ if not global_ignore_file_paths: global_ignore_file_paths = [] diff --git a/cycode/cli/utils/jwt_utils.py b/cycode/cli/utils/jwt_utils.py index 7bb7df62..c87b7c48 100644 --- a/cycode/cli/utils/jwt_utils.py +++ b/cycode/cli/utils/jwt_utils.py @@ -1,11 +1,11 @@ -from typing import Optional, Tuple +from typing import Optional import jwt _JWT_PAYLOAD_POSSIBLE_USER_ID_FIELD_NAMES = ('userId', 'internalId', 'token-user-id') -def get_user_and_tenant_ids_from_access_token(access_token: str) -> Tuple[Optional[str], Optional[str]]: +def get_user_and_tenant_ids_from_access_token(access_token: str) -> tuple[Optional[str], Optional[str]]: payload = jwt.decode(access_token, options={'verify_signature': False}) user_id = None diff --git a/cycode/cli/utils/path_utils.py b/cycode/cli/utils/path_utils.py index 3f670dd4..7d525e56 100644 --- a/cycode/cli/utils/path_utils.py +++ b/cycode/cli/utils/path_utils.py @@ -1,7 +1,7 @@ import json import os -from functools import lru_cache -from typing import TYPE_CHECKING, AnyStr, List, Optional, Union +from functools import cache +from typing import TYPE_CHECKING, AnyStr, Optional, Union import typer from binaryornot.helpers import is_binary_string @@ -12,7 +12,7 @@ from os import PathLike -@lru_cache(maxsize=None) +@cache def is_sub_path(path: str, sub_path: str) -> bool: try: common_path = os.path.commonpath([get_absolute_path(path), get_absolute_path(sub_path)]) @@ -35,7 +35,7 @@ def _get_starting_chunk(filename: str, length: int = 1024) -> Optional[bytes]: try: with open(filename, 'rb') as f: return f.read(length) - except IOError as e: + except OSError as e: logger.debug('Failed to read the starting chunk from file: %s', filename, exc_info=e) return None @@ -68,7 +68,7 @@ def get_file_dir(path: str) -> str: return os.path.dirname(path) -def get_immediate_subdirectories(path: str) -> List[str]: +def get_immediate_subdirectories(path: str) -> list[str]: return [f.name for f in os.scandir(path) if f.is_dir()] @@ -78,7 +78,7 @@ def join_paths(path: str, filename: str) -> str: def get_file_content(file_path: Union[str, 'PathLike']) -> Optional[AnyStr]: try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, encoding='UTF-8') as f: return f.read() except (FileNotFoundError, UnicodeDecodeError): return None diff --git a/cycode/cli/utils/progress_bar.py b/cycode/cli/utils/progress_bar.py index 054d5cf8..7c2de487 100644 --- a/cycode/cli/utils/progress_bar.py +++ b/cycode/cli/utils/progress_bar.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from enum import auto -from typing import Dict, NamedTuple, Optional +from typing import NamedTuple, Optional from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn @@ -38,7 +38,7 @@ class ProgressBarSectionInfo(NamedTuple): TimeElapsedColumn(), ) -ProgressBarSections = Dict[ProgressBarSection, ProgressBarSectionInfo] +ProgressBarSections = dict[ProgressBarSection, ProgressBarSectionInfo] class ScanProgressBarSection(ProgressBarSection): @@ -138,8 +138,8 @@ def __init__(self, progress_bar_sections: ProgressBarSections) -> None: self._progress_bar_sections = progress_bar_sections - self._section_lengths: Dict[ProgressBarSection, int] = {} - self._section_values: Dict[ProgressBarSection, int] = {} + self._section_lengths: dict[ProgressBarSection, int] = {} + self._section_values: dict[ProgressBarSection, int] = {} self._current_section_value = 0 self._current_section: ProgressBarSectionInfo = _get_initial_section(self._progress_bar_sections) @@ -195,7 +195,7 @@ def _increment_section_value(self, section: 'ProgressBarSection', value: int) -> ) def _rerender_progress_bar(self) -> None: - """Used to update label right after changing the progress bar section.""" + """Use to update label right after changing the progress bar section.""" self._progress_bar_update() def _increment_progress(self, section: 'ProgressBarSection') -> None: diff --git a/cycode/cli/utils/scan_batch.py b/cycode/cli/utils/scan_batch.py index 45e4d120..8bfd7ed0 100644 --- a/cycode/cli/utils/scan_batch.py +++ b/cycode/cli/utils/scan_batch.py @@ -1,6 +1,6 @@ import os from multiprocessing.pool import ThreadPool -from typing import TYPE_CHECKING, Callable, Dict, List, Tuple +from typing import TYPE_CHECKING, Callable from cycode.cli import consts from cycode.cli.models import Document @@ -45,8 +45,8 @@ def _get_max_batch_files_count(_: str) -> int: def split_documents_into_batches( scan_type: str, - documents: List[Document], -) -> List[List[Document]]: + documents: list[Document], +) -> list[list[Document]]: max_size = _get_max_batch_size(scan_type) max_files_count = _get_max_batch_files_count(scan_type) @@ -107,11 +107,11 @@ def _get_threads_count() -> int: def run_parallel_batched_scan( - scan_function: Callable[[List[Document]], Tuple[str, 'CliError', 'LocalScanResult']], + scan_function: Callable[[list[Document]], tuple[str, 'CliError', 'LocalScanResult']], scan_type: str, - documents: List[Document], + documents: list[Document], progress_bar: 'BaseProgressBar', -) -> Tuple[Dict[str, 'CliError'], List['LocalScanResult']]: +) -> tuple[dict[str, 'CliError'], list['LocalScanResult']]: # batching is disabled for SCA; requested by Mor batches = [documents] if scan_type == consts.SCA_SCAN_TYPE else split_documents_into_batches(scan_type, documents) @@ -124,8 +124,8 @@ def run_parallel_batched_scan( # the progress bar could be significant improved (be more dynamic) in the future threads_count = _get_threads_count() - local_scan_results: List['LocalScanResult'] = [] - cli_errors: Dict[str, 'CliError'] = {} + local_scan_results: list[LocalScanResult] = [] + cli_errors: dict[str, CliError] = {} logger.debug('Running parallel batched scan, %s', {'threads_count': threads_count, 'batches_count': len(batches)}) diff --git a/cycode/cli/utils/shell_executor.py b/cycode/cli/utils/shell_executor.py index 812fee1f..db0331da 100644 --- a/cycode/cli/utils/shell_executor.py +++ b/cycode/cli/utils/shell_executor.py @@ -1,5 +1,5 @@ import subprocess -from typing import List, Optional, Union +from typing import Optional, Union import click import typer @@ -13,7 +13,7 @@ def shell( - command: Union[str, List[str]], + command: Union[str, list[str]], timeout: int = _SUBPROCESS_DEFAULT_TIMEOUT_SEC, working_directory: Optional[str] = None, ) -> Optional[str]: diff --git a/cycode/cli/utils/task_timer.py b/cycode/cli/utils/task_timer.py index 29e65dc8..4b5e903e 100644 --- a/cycode/cli/utils/task_timer.py +++ b/cycode/cli/utils/task_timer.py @@ -1,19 +1,18 @@ from _thread import interrupt_main from threading import Event, Thread from types import TracebackType -from typing import Callable, Dict, List, Optional, Type +from typing import Callable, Optional class FunctionContext: - def __init__(self, function: Callable, args: Optional[List] = None, kwargs: Optional[Dict] = None) -> None: + def __init__(self, function: Callable, args: Optional[list] = None, kwargs: Optional[dict] = None) -> None: self.function = function self.args = args or [] self.kwargs = kwargs or {} class TimerThread(Thread): - """ - Custom thread class for executing timer in the background + """Custom thread class for executing timer in the background. Members: timeout - the amount of time to count until timeout in seconds @@ -43,8 +42,7 @@ def _call_quit_function(self) -> None: class TimeoutAfter: - """ - A task wrapper for controlling how much time a task should be run before timing out + """A task wrapper for controlling how much time a task should be run before timing out. Use Example: with TimeoutAfter(5, repeat_function=FunctionContext(x), repeat_interval=2): @@ -66,7 +64,7 @@ def __enter__(self) -> None: self.timer.start() def __exit__( - self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] + self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] ) -> None: if self.timeout: self.timer.stop() diff --git a/cycode/cli/utils/version_checker.py b/cycode/cli/utils/version_checker.py index 035b3595..47da17c4 100644 --- a/cycode/cli/utils/version_checker.py +++ b/cycode/cli/utils/version_checker.py @@ -2,7 +2,7 @@ import re import time from pathlib import Path -from typing import List, Optional, Tuple +from typing import Optional from cycode.cli.console import console from cycode.cli.user_settings.configuration_manager import ConfigurationManager @@ -11,8 +11,8 @@ def _compare_versions( - current_parts: List[int], - latest_parts: List[int], + current_parts: list[int], + latest_parts: list[int], current_is_pre: bool, latest_is_pre: bool, latest_version: str, @@ -33,6 +33,7 @@ def _compare_versions( Returns: str | None: The latest version string if an update is recommended, None if no update is needed + """ # If current is stable and latest is pre-release, don't suggest update if not current_is_pre and latest_is_pre: @@ -82,6 +83,7 @@ def get_latest_version(self) -> Optional[str]: Returns: str | None: The latest version string if successful, None if the request fails or the version information is not available. + """ try: response = self.get(f'{self.PYPI_PACKAGE_NAME}/json', timeout=self.PYPI_REQUEST_TIMEOUT) @@ -91,7 +93,7 @@ def get_latest_version(self) -> Optional[str]: return None @staticmethod - def _parse_version(version: str) -> Tuple[List[int], bool]: + def _parse_version(version: str) -> tuple[list[int], bool]: """Parse version string into components and identify if it's a pre-release. Extracts numeric version components and determines if the version is a pre-release @@ -104,6 +106,7 @@ def _parse_version(version: str) -> Tuple[List[int], bool]: tuple: A tuple containing: - List[int]: List of numeric version components - bool: True if this is a pre-release version, False otherwise + """ version_parts = [int(x) for x in re.findall(r'\d+', version)] is_prerelease = 'dev' in version @@ -122,6 +125,7 @@ def _should_check_update(self, is_prerelease: bool) -> bool: Returns: bool: True if an update check should be performed, False otherwise + """ if not os.path.exists(self.cache_file): return True @@ -148,7 +152,7 @@ def _update_last_check(self) -> None: os.makedirs(os.path.dirname(self.cache_file), exist_ok=True) with open(self.cache_file, 'w', encoding='UTF-8') as f: f.write(str(time.time())) - except IOError: + except OSError: pass def check_for_update(self, current_version: str, use_cache: bool = True) -> Optional[str]: @@ -163,6 +167,7 @@ def check_for_update(self, current_version: str, use_cache: bool = True) -> Opti Returns: str | None: The latest version string if an update is recommended, None if no update is needed or if check should be skipped + """ current_parts, current_is_pre = self._parse_version(current_version) @@ -192,6 +197,7 @@ def check_and_notify_update(self, current_version: str, use_cache: bool = True) Args: current_version: Current version of the CLI use_cache: If True, use the cached timestamp to determine if an update check is needed + """ latest_version = self.check_for_update(current_version, use_cache) should_update = bool(latest_version) diff --git a/cycode/cli/utils/yaml_utils.py b/cycode/cli/utils/yaml_utils.py index 388f3498..c89e1a5c 100644 --- a/cycode/cli/utils/yaml_utils.py +++ b/cycode/cli/utils/yaml_utils.py @@ -1,10 +1,11 @@ import os -from typing import Any, Dict, Hashable, TextIO +from collections.abc import Hashable +from typing import Any, TextIO import yaml -def _deep_update(source: Dict[Hashable, Any], overrides: Dict[Hashable, Any]) -> Dict[Hashable, Any]: +def _deep_update(source: dict[Hashable, Any], overrides: dict[Hashable, Any]) -> dict[Hashable, Any]: for key, value in overrides.items(): if isinstance(value, dict) and value: source[key] = _deep_update(source.get(key, {}), value) @@ -14,7 +15,7 @@ def _deep_update(source: Dict[Hashable, Any], overrides: Dict[Hashable, Any]) -> return source -def _yaml_safe_load(file: TextIO) -> Dict[Hashable, Any]: +def _yaml_safe_load(file: TextIO) -> dict[Hashable, Any]: # loader.get_single_data could return None loaded_file = yaml.safe_load(file) if loaded_file is None: @@ -23,18 +24,18 @@ def _yaml_safe_load(file: TextIO) -> Dict[Hashable, Any]: return loaded_file -def read_yaml_file(filename: str) -> Dict[Hashable, Any]: +def read_yaml_file(filename: str) -> dict[Hashable, Any]: if not os.path.exists(filename): return {} - with open(filename, 'r', encoding='UTF-8') as file: + with open(filename, encoding='UTF-8') as file: return _yaml_safe_load(file) -def write_yaml_file(filename: str, content: Dict[Hashable, Any]) -> None: +def write_yaml_file(filename: str, content: dict[Hashable, Any]) -> None: with open(filename, 'w', encoding='UTF-8') as file: yaml.safe_dump(content, file) -def update_yaml_file(filename: str, content: Dict[Hashable, Any]) -> None: +def update_yaml_file(filename: str, content: dict[Hashable, Any]) -> None: write_yaml_file(filename, _deep_update(read_yaml_file(filename), content)) diff --git a/cycode/cyclient/cycode_client_base.py b/cycode/cyclient/cycode_client_base.py index 37e9d4f6..4b2e2698 100644 --- a/cycode/cyclient/cycode_client_base.py +++ b/cycode/cyclient/cycode_client_base.py @@ -1,7 +1,7 @@ import os import platform import ssl -from typing import TYPE_CHECKING, Callable, ClassVar, Dict, Optional +from typing import TYPE_CHECKING, Callable, ClassVar, Optional import requests from requests import Response, exceptions @@ -14,7 +14,7 @@ RequestError, RequestHttpError, RequestSslError, - RequestTimeout, + RequestTimeoutError, ) from cycode.cyclient import config from cycode.cyclient.headers import get_cli_user_agent, get_correlation_id @@ -50,7 +50,7 @@ def _get_request_function() -> Callable: _REQUEST_ERRORS_TO_RETRY = ( - RequestTimeout, + RequestTimeoutError, RequestConnectionError, exceptions.ChunkedEncodingError, exceptions.ContentDecodingError, @@ -91,7 +91,7 @@ def _should_retry_exception(exception: BaseException) -> bool: class CycodeClientBase: - MANDATORY_HEADERS: ClassVar[Dict[str, str]] = { + MANDATORY_HEADERS: ClassVar[dict[str, str]] = { 'User-Agent': get_cli_user_agent(), 'X-Correlation-Id': get_correlation_id(), } @@ -160,7 +160,7 @@ def _execute( except Exception as e: self._handle_exception(e) - def get_request_headers(self, additional_headers: Optional[dict] = None, **kwargs) -> Dict[str, str]: + def get_request_headers(self, additional_headers: Optional[dict] = None, **kwargs) -> dict[str, str]: if additional_headers is None: return self.MANDATORY_HEADERS.copy() return {**self.MANDATORY_HEADERS, **additional_headers} @@ -170,7 +170,7 @@ def build_full_url(self, url: str, endpoint: str) -> str: def _handle_exception(self, e: Exception) -> None: if isinstance(e, exceptions.Timeout): - raise RequestTimeout from e + raise RequestTimeoutError from e if isinstance(e, exceptions.HTTPError): raise self._get_http_exception(e) from e if isinstance(e, exceptions.SSLError): diff --git a/cycode/cyclient/cycode_dev_based_client.py b/cycode/cyclient/cycode_dev_based_client.py index 347797c3..d8fe1cab 100644 --- a/cycode/cyclient/cycode_dev_based_client.py +++ b/cycode/cyclient/cycode_dev_based_client.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Optional from cycode.cyclient.config import dev_tenant_id from cycode.cyclient.cycode_client_base import CycodeClientBase @@ -12,7 +12,7 @@ class CycodeDevBasedClient(CycodeClientBase): def __init__(self, api_url: str) -> None: super().__init__(api_url) - def get_request_headers(self, additional_headers: Optional[dict] = None, **_) -> Dict[str, str]: + def get_request_headers(self, additional_headers: Optional[dict] = None, **_) -> dict[str, str]: headers = super().get_request_headers(additional_headers=additional_headers) headers['X-Tenant-Id'] = dev_tenant_id diff --git a/cycode/cyclient/headers.py b/cycode/cyclient/headers.py index 76716826..5d10f69b 100644 --- a/cycode/cyclient/headers.py +++ b/cycode/cyclient/headers.py @@ -35,6 +35,7 @@ def get_correlation_id(self) -> str: Used across all requests to correlate logs and metrics. It doesn't depend on client instances. Lifetime is the same as the process. + """ if self._id is None: # example: 16fd2706-8baf-433b-82eb-8c7fada847da diff --git a/cycode/cyclient/models.py b/cycode/cyclient/models.py index 2c0f53d7..70e3e551 100644 --- a/cycode/cyclient/models.py +++ b/cycode/cyclient/models.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Dict, List, Optional +from typing import Any, Optional from marshmallow import EXCLUDE, Schema, fields, post_load @@ -47,12 +47,12 @@ class Meta: detection_rule_id = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> Detection: + def build_dto(self, data: dict[str, Any], **_) -> Detection: return Detection(**data) class DetectionsPerFile(Schema): - def __init__(self, file_name: str, detections: List[Detection], commit_id: Optional[str] = None) -> None: + def __init__(self, file_name: str, detections: list[Detection], commit_id: Optional[str] = None) -> None: super().__init__() self.file_name = file_name self.detections = detections @@ -63,7 +63,7 @@ class ZippedFileScanResult(Schema): def __init__( self, did_detect: bool, - detections_per_file: List[DetectionsPerFile], + detections_per_file: list[DetectionsPerFile], report_url: Optional[str] = None, scan_id: Optional[str] = None, err: Optional[str] = None, @@ -81,7 +81,7 @@ def __init__( self, did_detect: bool, scan_id: Optional[str] = None, - detections: Optional[List[Detection]] = None, + detections: Optional[list[Detection]] = None, err: Optional[str] = None, ) -> None: super().__init__() @@ -101,7 +101,7 @@ class Meta: err = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'ScanResult': + def build_dto(self, data: dict[str, Any], **_) -> 'ScanResult': return ScanResult(**data) @@ -120,7 +120,7 @@ class Meta: err = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'ScanInitializationResponse': + def build_dto(self, data: dict[str, Any], **_) -> 'ScanInitializationResponse': return ScanInitializationResponse(**data) @@ -154,7 +154,7 @@ class ScanReportUrlResponseSchema(Schema): report_url = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'ScanReportUrlResponse': + def build_dto(self, data: dict[str, Any], **_) -> 'ScanReportUrlResponse': return ScanReportUrlResponse(**data) @@ -171,12 +171,12 @@ class Meta: err = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'ScanDetailsResponse': + def build_dto(self, data: dict[str, Any], **_) -> 'ScanDetailsResponse': return ScanDetailsResponse(**data) class K8SResource: - def __init__(self, name: str, resource_type: str, namespace: str, content: Dict) -> None: + def __init__(self, name: str, resource_type: str, namespace: str, content: dict) -> None: super().__init__() self.name = name self.type = resource_type @@ -201,7 +201,7 @@ def to_json(self) -> dict: # FIXME(MarshalX): rename to to_dict? class ResourcesCollection: - def __init__(self, resource_type: str, namespace: str, resources: List[K8SResource], total_count: int) -> None: + def __init__(self, resource_type: str, namespace: str, resources: list[K8SResource], total_count: int) -> None: super().__init__() self.type = resource_type self.namespace = namespace @@ -240,7 +240,7 @@ def __init__(self, name: str, kind: str) -> None: self.kind = kind def __str__(self) -> str: - return 'Name: {0}, Kind: {1}'.format(self.name, self.kind) + return f'Name: {self.name}, Kind: {self.kind}' class AuthenticationSession(Schema): @@ -256,7 +256,7 @@ class Meta: session_id = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'AuthenticationSession': + def build_dto(self, data: dict[str, Any], **_) -> 'AuthenticationSession': return AuthenticationSession(**data) @@ -277,7 +277,7 @@ class Meta: description = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'ApiToken': + def build_dto(self, data: dict[str, Any], **_) -> 'ApiToken': return ApiToken(**data) @@ -296,7 +296,7 @@ class Meta: api_token = fields.Nested(ApiTokenSchema, allow_none=True) @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'ApiTokenGenerationPollingResponse': + def build_dto(self, data: dict[str, Any], **_) -> 'ApiTokenGenerationPollingResponse': return ApiTokenGenerationPollingResponse(**data) @@ -307,7 +307,7 @@ class UserAgentOptionScheme(Schema): env_version = fields.String(required=True) # ex. 1.78.2 @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'UserAgentOption': + def build_dto(self, data: dict[str, Any], **_) -> 'UserAgentOption': return UserAgentOption(**data) @@ -349,7 +349,7 @@ class Meta: size = fields.Integer() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> SbomReportStorageDetails: + def build_dto(self, data: dict[str, Any], **_) -> SbomReportStorageDetails: return SbomReportStorageDetails(**data) @@ -373,13 +373,13 @@ class Meta: storage_details = fields.Nested(SbomReportStorageDetailsSchema, allow_none=True) @post_load - def build_dto(self, data: Dict[str, Any], **_) -> ReportExecution: + def build_dto(self, data: dict[str, Any], **_) -> ReportExecution: return ReportExecution(**data) @dataclass class SbomReport: - report_executions: List[ReportExecution] + report_executions: list[ReportExecution] class RequestedSbomReportResultSchema(Schema): @@ -389,7 +389,7 @@ class Meta: report_executions = fields.List(fields.Nested(ReportExecutionSchema)) @post_load - def build_dto(self, data: Dict[str, Any], **_) -> SbomReport: + def build_dto(self, data: dict[str, Any], **_) -> SbomReport: return SbomReport(**data) @@ -405,13 +405,13 @@ class Meta: severity = fields.String() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> ClassificationData: + def build_dto(self, data: dict[str, Any], **_) -> ClassificationData: return ClassificationData(**data) @dataclass class DetectionRule: - classification_data: List[ClassificationData] + classification_data: list[ClassificationData] detection_rule_id: str custom_remediation_guidelines: Optional[str] = None remediation_guidelines: Optional[str] = None @@ -433,14 +433,14 @@ class Meta: display_name = fields.String(allow_none=True) @post_load - def build_dto(self, data: Dict[str, Any], **_) -> DetectionRule: + def build_dto(self, data: dict[str, Any], **_) -> DetectionRule: return DetectionRule(**data) @dataclass class ScanResultsSyncFlow: id: str - detection_messages: List[Dict] + detection_messages: list[dict] class ScanResultsSyncFlowSchema(Schema): @@ -451,7 +451,7 @@ class Meta: detection_messages = fields.List(fields.Dict()) @post_load - def build_dto(self, data: Dict[str, Any], **_) -> ScanResultsSyncFlow: + def build_dto(self, data: dict[str, Any], **_) -> ScanResultsSyncFlow: return ScanResultsSyncFlow(**data) @@ -489,5 +489,5 @@ class Meta: ai_large_language_model = fields.Boolean() @post_load - def build_dto(self, data: Dict[str, Any], **_) -> 'SupportedModulesPreferences': + def build_dto(self, data: dict[str, Any], **_) -> 'SupportedModulesPreferences': return SupportedModulesPreferences(**data) diff --git a/cycode/cyclient/report_client.py b/cycode/cyclient/report_client.py index fa8e0c3f..e8107827 100644 --- a/cycode/cyclient/report_client.py +++ b/cycode/cyclient/report_client.py @@ -1,6 +1,6 @@ import dataclasses import json -from typing import List, Optional +from typing import Optional from requests import Response @@ -97,5 +97,5 @@ def parse_requested_sbom_report_response(response: Response) -> models.SbomRepor return models.RequestedSbomReportResultSchema().load(response.json()) @staticmethod - def parse_execution_status_response(response: Response) -> List[models.ReportExecutionSchema]: + def parse_execution_status_response(response: Response) -> list[models.ReportExecutionSchema]: return models.ReportExecutionSchema().load(response.json(), many=True) diff --git a/cycode/cyclient/scan_client.py b/cycode/cyclient/scan_client.py index 09908943..e0bf8131 100644 --- a/cycode/cyclient/scan_client.py +++ b/cycode/cyclient/scan_client.py @@ -1,6 +1,6 @@ import json from copy import deepcopy -from typing import TYPE_CHECKING, List, Set, Union +from typing import TYPE_CHECKING, Union from uuid import UUID from requests import Response @@ -135,7 +135,7 @@ def get_scan_details_path(self, scan_type: str, scan_id: str) -> str: return f'{self.get_scan_service_url_path(scan_type)}/{scan_id}' def get_scan_aggregation_report_url_path(self, aggregation_id: str, scan_type: str) -> str: - return f'{self.get_scan_service_url_path(scan_type)}' f'/reportUrlByAggregationId/{aggregation_id}' + return f'{self.get_scan_service_url_path(scan_type)}/reportUrlByAggregationId/{aggregation_id}' def get_scan_details(self, scan_type: str, scan_id: str) -> models.ScanDetailsResponse: path = self.get_scan_details_path(scan_type, scan_id) @@ -190,10 +190,10 @@ def _get_policy_type_by_scan_type(scan_type: str) -> str: return scan_type_to_policy_type[scan_type] @staticmethod - def parse_detection_rules_response(response: Response) -> List[models.DetectionRule]: + def parse_detection_rules_response(response: Response) -> list[models.DetectionRule]: return models.DetectionRuleSchema().load(response.json(), many=True) - def get_detection_rules(self, detection_rules_ids: Union[Set[str], List[str]]) -> List[models.DetectionRule]: + def get_detection_rules(self, detection_rules_ids: Union[set[str], list[str]]) -> list[models.DetectionRule]: response = self.scan_cycode_client.get( url_path=self.get_detection_rules_path(), params={'ids': detection_rules_ids}, @@ -208,7 +208,7 @@ def get_scan_detections_path(self) -> str: def get_scan_detections_list_path(self) -> str: return f'{self.get_scan_detections_path()}/detections' - def get_scan_raw_detections(self, scan_id: str) -> List[dict]: + def get_scan_raw_detections(self, scan_id: str) -> list[dict]: params = {'scan_id': scan_id} page_size = 200 diff --git a/cycode/logger.py b/cycode/logger.py index b63c796f..0ec6023f 100644 --- a/cycode/logger.py +++ b/cycode/logger.py @@ -1,6 +1,6 @@ import logging import sys -from typing import NamedTuple, Optional, Set, Union +from typing import NamedTuple, Optional, Union import click import typer @@ -42,7 +42,7 @@ class CreatedLogger(NamedTuple): control_level_in_runtime: bool -_CREATED_LOGGERS: Set[CreatedLogger] = set() +_CREATED_LOGGERS: set[CreatedLogger] = set() def get_logger_level() -> Optional[Union[int, str]]: diff --git a/poetry.lock b/poetry.lock index 12fdf576..65e6a971 100644 --- a/poetry.lock +++ b/poetry.lock @@ -828,30 +828,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.6.9" +version = "0.11.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, - {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, - {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, - {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, - {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, - {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, - {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, + {file = "ruff-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:d29e909d9a8d02f928d72ab7837b5cbc450a5bdf578ab9ebee3263d0a525091c"}, + {file = "ruff-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd1fb86b168ae349fb01dd497d83537b2c5541fe0626e70c786427dd8363aaee"}, + {file = "ruff-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3d7d2e140a6fbbc09033bce65bd7ea29d6a0adeb90b8430262fbacd58c38ada"}, + {file = "ruff-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4809df77de390a1c2077d9b7945d82f44b95d19ceccf0c287c56e4dc9b91ca64"}, + {file = "ruff-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3a0c2e169e6b545f8e2dba185eabbd9db4f08880032e75aa0e285a6d3f48201"}, + {file = "ruff-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49b888200a320dd96a68e86736cf531d6afba03e4f6cf098401406a257fcf3d6"}, + {file = "ruff-0.11.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2b19cdb9cf7dae00d5ee2e7c013540cdc3b31c4f281f1dacb5a799d610e90db4"}, + {file = "ruff-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64e0ee994c9e326b43539d133a36a455dbaab477bc84fe7bfbd528abe2f05c1e"}, + {file = "ruff-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bad82052311479a5865f52c76ecee5d468a58ba44fb23ee15079f17dd4c8fd63"}, + {file = "ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502"}, + {file = "ruff-0.11.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:169027e31c52c0e36c44ae9a9c7db35e505fee0b39f8d9fca7274a6305295a92"}, + {file = "ruff-0.11.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:305b93f9798aee582e91e34437810439acb28b5fc1fee6b8205c78c806845a94"}, + {file = "ruff-0.11.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a681db041ef55550c371f9cd52a3cf17a0da4c75d6bd691092dfc38170ebc4b6"}, + {file = "ruff-0.11.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:07f1496ad00a4a139f4de220b0c97da6d4c85e0e4aa9b2624167b7d4d44fd6b6"}, + {file = "ruff-0.11.7-py3-none-win32.whl", hash = "sha256:f25dfb853ad217e6e5f1924ae8a5b3f6709051a13e9dad18690de6c8ff299e26"}, + {file = "ruff-0.11.7-py3-none-win_amd64.whl", hash = "sha256:0a931d85959ceb77e92aea4bbedfded0a31534ce191252721128f77e5ae1f98a"}, + {file = "ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177"}, + {file = "ruff-0.11.7.tar.gz", hash = "sha256:655089ad3224070736dc32844fde783454f8558e71f501cb207485fe4eee23d4"}, ] [[package]] @@ -1123,4 +1123,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "83dddf9e309d442909a29574532caab06f9b0534f54083bc97ce994e8031d018" +content-hash = "14f258101aa534aadfc871aa5082ad773aa99873587c21c0598567435bfa5d9a" diff --git a/process_executable_file.py b/process_executable_file.py index ad4d702a..367bb18d 100755 --- a/process_executable_file.py +++ b/process_executable_file.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -""" -Used in the GitHub Actions workflow (build_executable.yml) to process the executable file. +"""Used in the GitHub Actions workflow (build_executable.yml) to process the executable file. + This script calculates hash and renames executable file depending on the OS, arch, and build mode. It also creates a file with the hash of the executable file. It uses SHA256 algorithm to calculate the hash. @@ -15,7 +15,7 @@ import shutil from pathlib import Path from string import Template -from typing import List, Tuple, Union +from typing import Union _ARCHIVE_FORMAT = 'zip' _HASH_FILE_EXT = '.sha256' @@ -27,7 +27,7 @@ _WINDOWS = 'windows' _WINDOWS_EXECUTABLE_SUFFIX = '.exe' -DirHashes = List[Tuple[str, str]] +DirHashes = list[tuple[str, str]] def get_hash_of_file(file_path: Union[str, Path]) -> str: @@ -35,7 +35,7 @@ def get_hash_of_file(file_path: Union[str, Path]) -> str: return hashlib.sha256(f.read()).hexdigest() -def get_hashes_of_many_files(root: str, file_paths: List[str]) -> DirHashes: +def get_hashes_of_many_files(root: str, file_paths: list[str]) -> DirHashes: hashes = [] for file_path in file_paths: diff --git a/pyproject.toml b/pyproject.toml index 994b0378..755d8207 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ pyinstaller = {version=">=5.13.2,<5.14.0", python=">=3.8,<3.13"} dunamai = ">=1.18.0,<1.22.0" [tool.poetry.group.dev.dependencies] -ruff = "0.6.9" +ruff = "0.11.7" [tool.pytest.ini_options] log_cli = true @@ -81,6 +81,7 @@ extend-select = [ "W", # pycodestyle warnings "F", # Pyflakes "I", # isort + "N", # pep8 naming "C90", # flake8-comprehensions "B", # flake8-bugbear "Q", # flake8-quotes @@ -100,19 +101,26 @@ extend-select = [ "RSE", "RUF", "SIM", + "T10", "T20", - "TCH", "TID", "YTT", + "LOG", "G", + "UP", + "DTZ", + "PYI", + "PT", + "SLOT", + "TC", ] ignore = [ "ANN002", # Missing type annotation for `*args` "ANN003", # Missing type annotation for `**kwargs` - "ANN101", # Missing type annotation for `self` in method - "ANN102", # Missing type annotation for `cls` in classmethod "ANN401", # Dynamically typed expressions (typing.Any) "ISC001", # Conflicts with ruff format + "S105", # False positives + "PT012", # `pytest.raises()` block should contain a single simple statement ] [tool.ruff.lint.flake8-quotes] diff --git a/tests/cli/commands/version/test_version_checker.py b/tests/cli/commands/version/test_version_checker.py index 926a21e8..14d6150e 100644 --- a/tests/cli/commands/version/test_version_checker.py +++ b/tests/cli/commands/version/test_version_checker.py @@ -71,7 +71,7 @@ def test_should_check_update_prerelease_daily(self, version_checker_cached: 'Ver assert version_checker_cached._should_check_update(is_prerelease=True) is True @pytest.mark.parametrize( - 'current_version, latest_version, expected_result', + ('current_version', 'latest_version', 'expected_result'), [ # Stable version comparisons ('1.2.3', '1.2.4', '1.2.4'), # Higher patch version diff --git a/tests/cli/exceptions/test_handle_scan_errors.py b/tests/cli/exceptions/test_handle_scan_errors.py index abd297db..ce72e9de 100644 --- a/tests/cli/exceptions/test_handle_scan_errors.py +++ b/tests/cli/exceptions/test_handle_scan_errors.py @@ -17,7 +17,7 @@ from _pytest.monkeypatch import MonkeyPatch -@pytest.fixture() +@pytest.fixture def ctx() -> typer.Context: ctx = typer.Context(click.Command('path'), obj={'verbose': False, 'output': OutputTypeOption.TEXT}) ctx.obj['console_printer'] = ConsolePrinter(ctx) @@ -25,7 +25,7 @@ def ctx() -> typer.Context: @pytest.mark.parametrize( - 'exception, expected_soft_fail', + ('exception', 'expected_soft_fail'), [ (custom_exceptions.RequestHttpError(400, 'msg', Response()), True), (custom_exceptions.ScanAsyncError('msg'), True), diff --git a/tests/cli/files_collector/test_walk_ignore.py b/tests/cli/files_collector/test_walk_ignore.py index b771cdf9..12b9d428 100644 --- a/tests/cli/files_collector/test_walk_ignore.py +++ b/tests/cli/files_collector/test_walk_ignore.py @@ -1,6 +1,6 @@ import os from os.path import normpath -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from cycode.cli.files_collector.walk_ignore import ( _collect_top_level_ignore_files, @@ -95,7 +95,7 @@ def test_collect_top_level_ignore_files(fs: 'FakeFilesystem') -> None: fs.create_file('/home/user/project/.gitignore', contents='*.pyc\n*.log') -def _collect_walk_ignore_files(path: str) -> List[str]: +def _collect_walk_ignore_files(path: str) -> list[str]: files = [] for root, _, filenames in walk_ignore(path): for filename in filenames: diff --git a/tests/cyclient/test_auth_client.py b/tests/cyclient/test_auth_client.py index 67147a6e..24d9b096 100644 --- a/tests/cyclient/test_auth_client.py +++ b/tests/cyclient/test_auth_client.py @@ -4,7 +4,7 @@ from requests import Timeout from cycode.cli.apps.auth.auth_manager import AuthManager -from cycode.cli.exceptions.custom_exceptions import CycodeError, RequestTimeout +from cycode.cli.exceptions.custom_exceptions import CycodeError, RequestTimeoutError from cycode.cyclient.auth_client import AuthClient from cycode.cyclient.models import ( ApiTokenGenerationPollingResponse, @@ -73,7 +73,7 @@ def test_start_session_timeout(client: AuthClient, start_url: str, code_challeng responses.add(responses.POST, start_url, body=timeout_error) - with pytest.raises(RequestTimeout): + with pytest.raises(RequestTimeoutError): client.start_session(code_challenge) diff --git a/tests/cyclient/test_scan_client.py b/tests/cyclient/test_scan_client.py index d81116fb..d6928118 100644 --- a/tests/cyclient/test_scan_client.py +++ b/tests/cyclient/test_scan_client.py @@ -1,5 +1,4 @@ import os -from typing import List, Tuple from uuid import uuid4 import pytest @@ -12,7 +11,7 @@ CycodeError, HttpUnauthorizedError, RequestConnectionError, - RequestTimeout, + RequestTimeoutError, ) from cycode.cli.files_collector.models.in_memory_zip import InMemoryZip from cycode.cli.models import Document @@ -28,7 +27,7 @@ ) -def zip_scan_resources(scan_type: ScanTypeOption, scan_client: ScanClient) -> Tuple[str, InMemoryZip]: +def zip_scan_resources(scan_type: ScanTypeOption, scan_client: ScanClient) -> tuple[str, InMemoryZip]: url = get_zipped_file_scan_async_url(scan_type, scan_client) zip_file = get_test_zip_file(scan_type) @@ -37,11 +36,11 @@ def zip_scan_resources(scan_type: ScanTypeOption, scan_client: ScanClient) -> Tu def get_test_zip_file(scan_type: ScanTypeOption) -> InMemoryZip: # TODO(MarshalX): refactor scan_disk_files in code_scanner.py to reuse method here instead of this - test_documents: List[Document] = [] + test_documents: list[Document] = [] for root, _, files in os.walk(ZIP_CONTENT_PATH): for name in files: path = os.path.join(root, name) - with open(path, 'r', encoding='UTF-8') as f: + with open(path, encoding='UTF-8') as f: test_documents.append(Document(path, f.read(), is_git_diff_format=False)) from cycode.cli.files_collector.zip_documents import zip_documents @@ -132,7 +131,7 @@ def test_zipped_file_scan_async_timeout_error( responses.add(api_token_response) # mock token based client responses.add(method=responses.POST, url=url, body=timeout_error) - with pytest.raises(RequestTimeout): + with pytest.raises(RequestTimeoutError): scan_client.zipped_file_scan_async(zip_file=zip_file, scan_type=scan_type, scan_parameters={}) diff --git a/tests/test_performance_get_all_files.py b/tests/test_performance_get_all_files.py index 60155261..b0e8653d 100644 --- a/tests/test_performance_get_all_files.py +++ b/tests/test_performance_get_all_files.py @@ -3,17 +3,17 @@ import os import timeit from pathlib import Path -from typing import Dict, List, Tuple, Union +from typing import Union logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def filter_files(paths: List[Union[Path, str]]) -> List[str]: +def filter_files(paths: list[Union[Path, str]]) -> list[str]: return [str(path) for path in paths if os.path.isfile(path)] -def get_all_files_glob(path: Union[Path, str]) -> List[str]: +def get_all_files_glob(path: Union[Path, str]) -> list[str]: # DOESN'T RETURN HIDDEN FILES. CAN'T BE USED # and doesn't show the best performance if not str(path).endswith(os.sep): @@ -22,7 +22,7 @@ def get_all_files_glob(path: Union[Path, str]) -> List[str]: return filter_files(glob.glob(f'{path}**', recursive=True)) -def get_all_files_walk(path: str) -> List[str]: +def get_all_files_walk(path: str) -> list[str]: files = [] for root, _, filenames in os.walk(path): @@ -32,7 +32,7 @@ def get_all_files_walk(path: str) -> List[str]: return files -def get_all_files_listdir(path: str) -> List[str]: +def get_all_files_listdir(path: str) -> list[str]: files = [] def _(sub_path: str) -> None: @@ -50,12 +50,12 @@ def _(sub_path: str) -> None: return files -def get_all_files_rglob(path: str) -> List[str]: +def get_all_files_rglob(path: str) -> list[str]: return filter_files(list(Path(path).rglob(r'*'))) def test_get_all_files_performance(test_files_path: str) -> None: - results: Dict[str, Tuple[int, float]] = {} + results: dict[str, tuple[int, float]] = {} for func in { get_all_files_rglob, get_all_files_listdir, diff --git a/tests/user_settings/test_configuration_manager.py b/tests/user_settings/test_configuration_manager.py index 50251340..5aa7f6a8 100644 --- a/tests/user_settings/test_configuration_manager.py +++ b/tests/user_settings/test_configuration_manager.py @@ -1,6 +1,5 @@ from typing import TYPE_CHECKING, Optional - -from mock import Mock +from unittest.mock import Mock from cycode.cli.consts import DEFAULT_CYCODE_API_URL from cycode.cli.user_settings.configuration_manager import ConfigurationManager diff --git a/tests/utils/test_ignore_utils.py b/tests/utils/test_ignore_utils.py index 563c11a9..6988e1aa 100644 --- a/tests/utils/test_ignore_utils.py +++ b/tests/utils/test_ignore_utils.py @@ -87,9 +87,9 @@ def test_translate(self) -> None: for pattern, regex in TRANSLATE_TESTS: if re.escape(b'/') == b'/': regex = regex.replace(b'\\/', b'/') - assert ( - translate(pattern) == regex - ), f'orig pattern: {pattern!r}, regex: {translate(pattern)!r}, expected: {regex!r}' + assert translate(pattern) == regex, ( + f'orig pattern: {pattern!r}, regex: {translate(pattern)!r}, expected: {regex!r}' + ) def test_read_file(self) -> None: f = BytesIO(