diff --git a/.github/workflows/build_executable.yml b/.github/workflows/build_executable.yml index 4ff61810..717b18e4 100644 --- a/.github/workflows/build_executable.yml +++ b/.github/workflows/build_executable.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-20.04, macos-12, macos-14, windows-2019 ] + os: [ ubuntu-20.04, macos-13, macos-14, windows-2019 ] mode: [ 'onefile', 'onedir' ] exclude: - os: ubuntu-20.04 diff --git a/cycode/cli/commands/ai_remediation/__init__.py b/cycode/cli/commands/ai_remediation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cycode/cli/commands/ai_remediation/ai_remediation_command.py b/cycode/cli/commands/ai_remediation/ai_remediation_command.py new file mode 100644 index 00000000..608fc9f4 --- /dev/null +++ b/cycode/cli/commands/ai_remediation/ai_remediation_command.py @@ -0,0 +1,67 @@ +import os + +import click +from patch_ng import fromstring +from rich.console import Console +from rich.markdown import Markdown + +from cycode.cli.exceptions.handle_ai_remediation_errors import handle_ai_remediation_exception +from cycode.cli.models import CliResult +from cycode.cli.printers import ConsolePrinter +from cycode.cli.utils.get_api_client import get_scan_cycode_client + + +def _echo_remediation(context: click.Context, remediation_markdown: str, is_fix_available: bool) -> None: + printer = ConsolePrinter(context) + if printer.is_json_printer: + data = {'remediation': remediation_markdown, 'is_fix_available': is_fix_available} + printer.print_result(CliResult(success=True, message='Remediation fetched successfully', data=data)) + else: # text or table + Console().print(Markdown(remediation_markdown)) + + +def _apply_fix(context: click.Context, diff: str, is_fix_available: bool) -> None: + printer = ConsolePrinter(context) + if not is_fix_available: + printer.print_result(CliResult(success=False, message='Fix is not available for this violation')) + return + + patch = fromstring(diff.encode('UTF-8')) + if patch is False: + printer.print_result(CliResult(success=False, message='Failed to parse fix diff')) + return + + is_fix_applied = patch.apply(root=os.getcwd(), strip=0) + if is_fix_applied: + printer.print_result(CliResult(success=True, message='Fix applied successfully')) + else: + printer.print_result(CliResult(success=False, message='Failed to apply fix')) + + +@click.command(short_help='Get AI remediation (INTERNAL).', hidden=True) +@click.argument('detection_id', nargs=1, type=click.UUID, required=True) +@click.option( + '--fix', + is_flag=True, + default=False, + help='Apply fixes to resolve violations. Fix is not available for all violations.', + type=click.BOOL, + required=False, +) +@click.pass_context +def ai_remediation_command(context: click.Context, detection_id: str, fix: bool) -> None: + client = get_scan_cycode_client() + + try: + remediation_markdown = client.get_ai_remediation(detection_id) + fix_diff = client.get_ai_remediation(detection_id, fix=True) + is_fix_available = bool(fix_diff) # exclude empty string, None, etc. + + if fix: + _apply_fix(context, fix_diff, is_fix_available) + else: + _echo_remediation(context, remediation_markdown, is_fix_available) + except Exception as err: + handle_ai_remediation_exception(context, err) + + context.exit() diff --git a/cycode/cli/commands/main_cli.py b/cycode/cli/commands/main_cli.py index 67bb8171..f97e0749 100644 --- a/cycode/cli/commands/main_cli.py +++ b/cycode/cli/commands/main_cli.py @@ -3,6 +3,7 @@ import click +from cycode.cli.commands.ai_remediation.ai_remediation_command import ai_remediation_command from cycode.cli.commands.auth.auth_command import auth_command from cycode.cli.commands.configure.configure_command import configure_command from cycode.cli.commands.ignore.ignore_command import ignore_command @@ -30,6 +31,7 @@ 'auth': auth_command, 'version': version_command, 'status': status_command, + 'ai_remediation': ai_remediation_command, }, context_settings=CLI_CONTEXT_SETTINGS, ) diff --git a/cycode/cli/commands/version/version_command.py b/cycode/cli/commands/version/version_command.py index 55755e24..107aedbc 100644 --- a/cycode/cli/commands/version/version_command.py +++ b/cycode/cli/commands/version/version_command.py @@ -6,7 +6,7 @@ from cycode.cli.consts import PROGRAM_NAME -@click.command(short_help='Show the CLI version and exit.') +@click.command(short_help='Show the CLI version and exit. Use `cycode status` instead.', deprecated=True) @click.pass_context def version_command(context: click.Context) -> None: output = context.obj['output'] diff --git a/cycode/cli/consts.py b/cycode/cli/consts.py index 4304580a..cd546075 100644 --- a/cycode/cli/consts.py +++ b/cycode/cli/consts.py @@ -159,6 +159,10 @@ SYNC_SCAN_TIMEOUT_IN_SECONDS_ENV_VAR_NAME = 'SYNC_SCAN_TIMEOUT_IN_SECONDS' DEFAULT_SYNC_SCAN_TIMEOUT_IN_SECONDS = 180 +# ai remediation +AI_REMEDIATION_TIMEOUT_IN_SECONDS_ENV_VAR_NAME = 'AI_REMEDIATION_TIMEOUT_IN_SECONDS' +DEFAULT_AI_REMEDIATION_TIMEOUT_IN_SECONDS = 60 + # report with polling REPORT_POLLING_WAIT_INTERVAL_IN_SECONDS = 5 DEFAULT_REPORT_POLLING_TIMEOUT_IN_SECONDS = 600 diff --git a/cycode/cli/exceptions/common.py b/cycode/cli/exceptions/common.py new file mode 100644 index 00000000..51433af7 --- /dev/null +++ b/cycode/cli/exceptions/common.py @@ -0,0 +1,37 @@ +from typing import Optional + +import click + +from cycode.cli.models import CliError, CliErrors +from cycode.cli.printers import ConsolePrinter +from cycode.cli.sentry import capture_exception + + +def handle_errors( + context: click.Context, err: BaseException, cli_errors: CliErrors, *, return_exception: bool = False +) -> Optional['CliError']: + ConsolePrinter(context).print_exception(err) + + if type(err) in cli_errors: + error = cli_errors[type(err)] + + if error.soft_fail is True: + context.obj['soft_fail'] = True + + if return_exception: + return error + + ConsolePrinter(context).print_error(error) + return None + + if isinstance(err, click.ClickException): + raise err + + capture_exception(err) + + unknown_error = CliError(code='unknown_error', message=str(err)) + if return_exception: + return unknown_error + + ConsolePrinter(context).print_error(unknown_error) + exit(1) diff --git a/cycode/cli/exceptions/handle_ai_remediation_errors.py b/cycode/cli/exceptions/handle_ai_remediation_errors.py new file mode 100644 index 00000000..ba46cbf7 --- /dev/null +++ b/cycode/cli/exceptions/handle_ai_remediation_errors.py @@ -0,0 +1,22 @@ +import click + +from cycode.cli.exceptions.common import handle_errors +from cycode.cli.exceptions.custom_exceptions import KNOWN_USER_FRIENDLY_REQUEST_ERRORS, RequestHttpError +from cycode.cli.models import CliError, CliErrors + + +class AiRemediationNotFoundError(Exception): ... + + +def handle_ai_remediation_exception(context: click.Context, err: Exception) -> None: + if isinstance(err, RequestHttpError) and err.status_code == 404: + err = AiRemediationNotFoundError() + + errors: CliErrors = { + **KNOWN_USER_FRIENDLY_REQUEST_ERRORS, + AiRemediationNotFoundError: CliError( + code='ai_remediation_not_found', + message='The AI remediation was not found. Please try different detection ID', + ), + } + handle_errors(context, err, errors) diff --git a/cycode/cli/exceptions/handle_report_sbom_errors.py b/cycode/cli/exceptions/handle_report_sbom_errors.py index bfd407a0..70cf6277 100644 --- a/cycode/cli/exceptions/handle_report_sbom_errors.py +++ b/cycode/cli/exceptions/handle_report_sbom_errors.py @@ -1,17 +1,12 @@ -from typing import Optional - import click from cycode.cli.exceptions import custom_exceptions +from cycode.cli.exceptions.common import handle_errors from cycode.cli.exceptions.custom_exceptions import KNOWN_USER_FRIENDLY_REQUEST_ERRORS from cycode.cli.models import CliError, CliErrors -from cycode.cli.printers import ConsolePrinter -from cycode.cli.sentry import capture_exception - -def handle_report_exception(context: click.Context, err: Exception) -> Optional[CliError]: - ConsolePrinter(context).print_exception() +def handle_report_exception(context: click.Context, err: Exception) -> None: errors: CliErrors = { **KNOWN_USER_FRIENDLY_REQUEST_ERRORS, custom_exceptions.ScanAsyncError: CliError( @@ -25,16 +20,4 @@ def handle_report_exception(context: click.Context, err: Exception) -> Optional[ 'Please try again by executing the `cycode report` command', ), } - - if type(err) in errors: - error = errors[type(err)] - - ConsolePrinter(context).print_error(error) - return None - - if isinstance(err, click.ClickException): - raise err - - capture_exception(err) - - raise click.ClickException(str(err)) + handle_errors(context, err, errors) diff --git a/cycode/cli/exceptions/handle_scan_errors.py b/cycode/cli/exceptions/handle_scan_errors.py index 2790418a..550e6879 100644 --- a/cycode/cli/exceptions/handle_scan_errors.py +++ b/cycode/cli/exceptions/handle_scan_errors.py @@ -3,20 +3,17 @@ import click from cycode.cli.exceptions import custom_exceptions +from cycode.cli.exceptions.common import handle_errors from cycode.cli.exceptions.custom_exceptions import KNOWN_USER_FRIENDLY_REQUEST_ERRORS from cycode.cli.models import CliError, CliErrors -from cycode.cli.printers import ConsolePrinter -from cycode.cli.sentry import capture_exception from cycode.cli.utils.git_proxy import git_proxy def handle_scan_exception( - context: click.Context, e: Exception, *, return_exception: bool = False + context: click.Context, err: Exception, *, return_exception: bool = False ) -> Optional[CliError]: context.obj['did_fail'] = True - ConsolePrinter(context).print_exception(e) - errors: CliErrors = { **KNOWN_USER_FRIENDLY_REQUEST_ERRORS, custom_exceptions.ScanAsyncError: CliError( @@ -35,7 +32,7 @@ def handle_scan_exception( custom_exceptions.TfplanKeyError: CliError( soft_fail=True, code='key_error', - message=f'\n{e!s}\n' + message=f'\n{err!s}\n' 'A crucial field is missing in your terraform plan file. ' 'Please make sure that your file is well formed ' 'and execute the scan again', @@ -48,26 +45,4 @@ def handle_scan_exception( ), } - if type(e) in errors: - error = errors[type(e)] - - if error.soft_fail is True: - context.obj['soft_fail'] = True - - if return_exception: - return error - - ConsolePrinter(context).print_error(error) - return None - - if isinstance(e, click.ClickException): - raise e - - capture_exception(e) - - unknown_error = CliError(code='unknown_error', message=str(e)) - if return_exception: - return unknown_error - - ConsolePrinter(context).print_error(unknown_error) - exit(1) + return handle_errors(context, err, errors, return_exception=return_exception) diff --git a/cycode/cli/models.py b/cycode/cli/models.py index 39c07e44..7020ade3 100644 --- a/cycode/cli/models.py +++ b/cycode/cli/models.py @@ -63,7 +63,7 @@ class CliError(NamedTuple): soft_fail: bool = False -CliErrors = Dict[Type[Exception], CliError] +CliErrors = Dict[Type[BaseException], CliError] class CliResult(NamedTuple): diff --git a/cycode/cli/printers/console_printer.py b/cycode/cli/printers/console_printer.py index ad473560..1f70836c 100644 --- a/cycode/cli/printers/console_printer.py +++ b/cycode/cli/printers/console_printer.py @@ -60,3 +60,15 @@ def print_exception(self, e: Optional[BaseException] = None, force_print: bool = """Print traceback message in stderr if verbose mode is set.""" if force_print or self.context.obj.get('verbose', False): self._printer_class(self.context).print_exception(e) + + @property + def is_json_printer(self) -> bool: + return self._printer_class == JsonPrinter + + @property + def is_table_printer(self) -> bool: + return self._printer_class == TablePrinter + + @property + def is_text_printer(self) -> bool: + return self._printer_class == TextPrinter diff --git a/cycode/cli/user_settings/configuration_manager.py b/cycode/cli/user_settings/configuration_manager.py index 909a97f0..b83bed32 100644 --- a/cycode/cli/user_settings/configuration_manager.py +++ b/cycode/cli/user_settings/configuration_manager.py @@ -113,6 +113,13 @@ def get_sync_scan_timeout_in_seconds(self) -> int: ) ) + def get_ai_remediation_timeout_in_seconds(self) -> int: + return int( + self._get_value_from_environment_variables( + consts.AI_REMEDIATION_TIMEOUT_IN_SECONDS_ENV_VAR_NAME, consts.DEFAULT_AI_REMEDIATION_TIMEOUT_IN_SECONDS + ) + ) + def get_report_polling_timeout_in_seconds(self) -> int: return int( self._get_value_from_environment_variables( diff --git a/cycode/cyclient/models.py b/cycode/cyclient/models.py index a2162ba8..2433ef6c 100644 --- a/cycode/cyclient/models.py +++ b/cycode/cyclient/models.py @@ -13,8 +13,10 @@ def __init__( detection_details: dict, detection_rule_id: str, severity: Optional[str] = None, + id: Optional[str] = None, ) -> None: super().__init__() + self.id = id self.message = message self.type = type self.severity = severity @@ -36,6 +38,7 @@ class DetectionSchema(Schema): class Meta: unknown = EXCLUDE + id = fields.String(missing=None) message = fields.String() type = fields.String() severity = fields.String(missing=None) diff --git a/cycode/cyclient/scan_client.py b/cycode/cyclient/scan_client.py index 744d73e2..b63f49e1 100644 --- a/cycode/cyclient/scan_client.py +++ b/cycode/cyclient/scan_client.py @@ -206,6 +206,28 @@ def get_supported_modules_preferences(self) -> models.SupportedModulesPreference response = self.scan_cycode_client.get(url_path='preferences/api/v1/supportedmodules') return models.SupportedModulesPreferencesSchema().load(response.json()) + @staticmethod + def get_ai_remediation_path(detection_id: str) -> str: + return f'scm-remediator/api/v1/ContentRemediation/preview/{detection_id}' + + def get_ai_remediation(self, detection_id: str, *, fix: bool = False) -> str: + path = self.get_ai_remediation_path(detection_id) + + data = { + 'resolving_parameters': { + 'get_diff': True, + 'use_code_snippet': True, + 'add_diff_header': True, + } + } + if not fix: + data['resolving_parameters']['remediation_action'] = 'ReplyWithRemediationDetails' + + response = self.scan_cycode_client.get( + url_path=path, json=data, timeout=configuration_manager.get_ai_remediation_timeout_in_seconds() + ) + return response.text.strip() + @staticmethod def _get_policy_type_by_scan_type(scan_type: str) -> str: scan_type_to_policy_type = { diff --git a/poetry.lock b/poetry.lock index a1d8c39f..1a755b08 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "altgraph" @@ -399,6 +399,30 @@ files = [ [package.dependencies] altgraph = ">=0.17" +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "marshmallow" version = "3.22.0" @@ -418,6 +442,17 @@ dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] tests = ["pytest", "pytz", "simplejson"] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mock" version = "4.0.3" @@ -436,13 +471,23 @@ test = ["pytest (<5.4)", "pytest-cov"] [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "patch-ng" +version = "1.18.1" +description = "Library to parse and apply unified diffs." +optional = false +python-versions = ">=3.6" +files = [ + {file = "patch-ng-1.18.1.tar.gz", hash = "sha256:52fd46ee46f6c8667692682c1fd7134edc65a2d2d084ebec1d295a6087fc0291"}, ] [[package]] @@ -482,6 +527,20 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyinstaller" version = "5.13.2" @@ -517,13 +576,13 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2024.8" +version = "2024.10" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.8" files = [ - {file = "pyinstaller_hooks_contrib-2024.8-py3-none-any.whl", hash = "sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a"}, - {file = "pyinstaller_hooks_contrib-2024.8.tar.gz", hash = "sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"}, + {file = "pyinstaller_hooks_contrib-2024.10-py3-none-any.whl", hash = "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10"}, + {file = "pyinstaller_hooks_contrib-2024.10.tar.gz", hash = "sha256:8a46655e5c5b0186b5e527399118a9b342f10513eb1425c483fa4f6d02e8800c"}, ] [package.dependencies] @@ -715,6 +774,25 @@ urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.6.9" @@ -744,13 +822,13 @@ files = [ [[package]] name = "sentry-sdk" -version = "2.16.0" +version = "2.19.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.16.0-py2.py3-none-any.whl", hash = "sha256:49139c31ebcd398f4f6396b18910610a0c1602f6e67083240c33019d1f6aa30c"}, - {file = "sentry_sdk-2.16.0.tar.gz", hash = "sha256:90f733b32e15dfc1999e6b7aca67a38688a567329de4d6e184154a73f96c6892"}, + {file = "sentry_sdk-2.19.0-py2.py3-none-any.whl", hash = "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b"}, + {file = "sentry_sdk-2.19.0.tar.gz", hash = "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36"}, ] [package.dependencies] @@ -776,14 +854,16 @@ grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] http2 = ["httpcore[http2] (==1.*)"] httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] -huggingface-hub = ["huggingface-hub (>=0.22)"] +huggingface-hub = ["huggingface_hub (>=0.22)"] langchain = ["langchain (>=0.0.210)"] +launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] litestar = ["litestar (>=2.0.0)"] loguru = ["loguru (>=0.5)"] openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +openfeature = ["openfeature-sdk (>=0.7.1)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] opentelemetry-experimental = ["opentelemetry-distro"] -pure-eval = ["asttokens", "executing", "pure-eval"] +pure-eval = ["asttokens", "executing", "pure_eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] @@ -796,23 +876,23 @@ tornado = ["tornado (>=6)"] [[package]] name = "setuptools" -version = "75.1.0" +version = "75.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, - {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, + {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, + {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "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)", "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)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "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)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "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", "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)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] [[package]] name = "six" @@ -849,13 +929,13 @@ files = [ [[package]] name = "tomli" -version = "2.0.2" +version = "2.1.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, ] [[package]] @@ -880,6 +960,17 @@ files = [ {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, ] +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +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"}, +] + [[package]] name = "urllib3" version = "1.26.19" @@ -918,4 +1009,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.14" -content-hash = "6e23c9650b529e0c928f90a17d549d73b8418e11a86c2a1c9213f7582faa7e17" +content-hash = "9ad1d7ff7f6e1dc4b43af55f5f034d051dde5205cf9ac247026f8e3c2f465f31" diff --git a/pyproject.toml b/pyproject.toml index 9c1e1f9a..adb99510 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,8 @@ requests = ">=2.32.2,<3.0" urllib3 = "1.26.19" # lock v1 to avoid issues with openssl and old Python versions (<3.9.11) on macOS sentry-sdk = ">=2.8.0,<3.0" pyjwt = ">=2.8.0,<3.0" +rich = ">=13.9.4, <14" +patch-ng = "1.18.1" [tool.poetry.group.test.dependencies] mock = ">=4.0.3,<4.1.0"