Skip to content

Commit e33af70

Browse files
committed
CM-45153, CM-45154, CM-45155, CM-45156, CM-45546 - Migrate CLI from Click to Typer
1 parent 84b8e34 commit e33af70

File tree

120 files changed

+1512
-1389
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+1512
-1389
lines changed

cycode/cli/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from cycode.cli.main import app
2+
3+
app()

cycode/cli/app.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import logging
2+
from typing import Annotated, Optional
3+
4+
import typer
5+
6+
from cycode import __version__
7+
from cycode.cli.apps import ai_remediation, auth, configure, ignore, report, scan, status
8+
from cycode.cli.cli_types import OutputTypeOption
9+
from cycode.cli.consts import CLI_CONTEXT_SETTINGS
10+
from cycode.cli.user_settings.configuration_manager import ConfigurationManager
11+
from cycode.cli.utils.progress_bar import SCAN_PROGRESS_BAR_SECTIONS, get_progress_bar
12+
from cycode.cli.utils.sentry import add_breadcrumb, init_sentry
13+
from cycode.cli.utils.version_checker import version_checker
14+
from cycode.cyclient.config import set_logging_level
15+
from cycode.cyclient.cycode_client_base import CycodeClientBase
16+
from cycode.cyclient.models import UserAgentOptionScheme
17+
18+
app = typer.Typer(
19+
pretty_exceptions_show_locals=False,
20+
pretty_exceptions_short=True,
21+
context_settings=CLI_CONTEXT_SETTINGS,
22+
rich_markup_mode='rich',
23+
)
24+
25+
app.add_typer(ai_remediation.app)
26+
app.add_typer(auth.app)
27+
app.add_typer(configure.app)
28+
app.add_typer(ignore.app)
29+
app.add_typer(report.app)
30+
app.add_typer(scan.app)
31+
app.add_typer(status.app)
32+
33+
34+
def check_latest_version_on_close(ctx: typer.Context) -> None:
35+
output = ctx.obj.get('output')
36+
# don't print anything if the output is JSON
37+
if output == OutputTypeOption.JSON:
38+
return
39+
40+
# we always want to check the latest version for "version" and "status" commands
41+
should_use_cache = ctx.invoked_subcommand not in {'version', 'status'}
42+
version_checker.check_and_notify_update(
43+
current_version=__version__, use_color=ctx.color, use_cache=should_use_cache
44+
)
45+
46+
47+
@app.callback()
48+
def app_callback(
49+
ctx: typer.Context,
50+
verbose: Annotated[bool, typer.Option('--verbose', '-v', help='Show detailed logs.')] = False,
51+
no_progress_meter: Annotated[
52+
bool, typer.Option('--no-progress-meter', help='Do not show the progress meter.')
53+
] = False,
54+
no_update_notifier: Annotated[
55+
bool, typer.Option('--no-update-notifier', help='Do not check CLI for updates.')
56+
] = False,
57+
output: Annotated[
58+
OutputTypeOption, typer.Option('--output', '-o', case_sensitive=False, help='Specify the output type.')
59+
] = OutputTypeOption.TEXT,
60+
user_agent: Annotated[
61+
Optional[str],
62+
typer.Option(hidden=True, help='Characteristic JSON object that lets servers identify the application.'),
63+
] = None,
64+
) -> None:
65+
init_sentry()
66+
add_breadcrumb('cycode')
67+
68+
ctx.ensure_object(dict)
69+
configuration_manager = ConfigurationManager()
70+
71+
verbose = verbose or configuration_manager.get_verbose_flag()
72+
ctx.obj['verbose'] = verbose
73+
if verbose:
74+
set_logging_level(logging.DEBUG)
75+
76+
ctx.obj['output'] = output
77+
if output == OutputTypeOption.JSON:
78+
no_progress_meter = True
79+
80+
ctx.obj['progress_bar'] = get_progress_bar(hidden=no_progress_meter, sections=SCAN_PROGRESS_BAR_SECTIONS)
81+
82+
if user_agent:
83+
user_agent_option = UserAgentOptionScheme().loads(user_agent)
84+
CycodeClientBase.enrich_user_agent(user_agent_option.user_agent_suffix)
85+
86+
if not no_update_notifier:
87+
ctx.call_on_close(lambda: check_latest_version_on_close(ctx))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import typer
2+
3+
from cycode.cli.apps.ai_remediation.ai_remediation_command import ai_remediation_command
4+
5+
app = typer.Typer()
6+
app.command(name='ai_remediation', short_help='Get AI remediation (INTERNAL).', hidden=True)(ai_remediation_command)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Annotated
2+
from uuid import UUID
3+
4+
import typer
5+
6+
from cycode.cli.apps.ai_remediation.apply_fix import apply_fix
7+
from cycode.cli.apps.ai_remediation.print_remediation import print_remediation
8+
from cycode.cli.exceptions.handle_ai_remediation_errors import handle_ai_remediation_exception
9+
from cycode.cli.utils.get_api_client import get_scan_cycode_client
10+
11+
12+
def ai_remediation_command(
13+
ctx: typer.Context,
14+
detection_id: Annotated[UUID, typer.Argument(help='Detection ID to get remediation for', show_default=False)],
15+
fix: Annotated[
16+
bool, typer.Option('--fix', help='Apply fixes to resolve violations. Note: fix could be not available.')
17+
] = False,
18+
) -> None:
19+
"""Get AI remediation (INTERNAL)."""
20+
client = get_scan_cycode_client()
21+
22+
try:
23+
remediation_markdown = client.get_ai_remediation(detection_id)
24+
fix_diff = client.get_ai_remediation(detection_id, fix=True)
25+
is_fix_available = bool(fix_diff) # exclude empty string, None, etc.
26+
27+
if fix:
28+
apply_fix(ctx, fix_diff, is_fix_available)
29+
else:
30+
print_remediation(ctx, remediation_markdown, is_fix_available)
31+
except Exception as err:
32+
handle_ai_remediation_exception(ctx, err)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import os
2+
3+
import typer
4+
from patch_ng import fromstring
5+
6+
from cycode.cli.models import CliResult
7+
from cycode.cli.printers import ConsolePrinter
8+
9+
10+
def apply_fix(ctx: typer.Context, diff: str, is_fix_available: bool) -> None:
11+
printer = ConsolePrinter(ctx)
12+
if not is_fix_available:
13+
printer.print_result(CliResult(success=False, message='Fix is not available for this violation'))
14+
return
15+
16+
patch = fromstring(diff.encode('UTF-8'))
17+
if patch is False:
18+
printer.print_result(CliResult(success=False, message='Failed to parse fix diff'))
19+
return
20+
21+
is_fix_applied = patch.apply(root=os.getcwd(), strip=0)
22+
if is_fix_applied:
23+
printer.print_result(CliResult(success=True, message='Fix applied successfully'))
24+
else:
25+
printer.print_result(CliResult(success=False, message='Failed to apply fix'))
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import typer
2+
from rich.console import Console
3+
from rich.markdown import Markdown
4+
5+
from cycode.cli.models import CliResult
6+
from cycode.cli.printers import ConsolePrinter
7+
8+
9+
def print_remediation(ctx: typer.Context, remediation_markdown: str, is_fix_available: bool) -> None:
10+
printer = ConsolePrinter(ctx)
11+
if printer.is_json_printer:
12+
data = {'remediation': remediation_markdown, 'is_fix_available': is_fix_available}
13+
printer.print_result(CliResult(success=True, message='Remediation fetched successfully', data=data))
14+
else: # text or table
15+
Console().print(Markdown(remediation_markdown))

cycode/cli/apps/auth/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import typer
2+
3+
from cycode.cli.apps.auth.auth_command import auth_command
4+
from cycode.cli.apps.auth.check_command import check_command
5+
6+
app = typer.Typer(
7+
name='auth',
8+
help='Authenticate your machine to associate the CLI with your Cycode account.',
9+
)
10+
app.callback(invoke_without_command=True)(auth_command)
11+
app.command(name='check')(check_command)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import typer
2+
3+
from cycode.cli.apps.auth.auth_manager import AuthManager
4+
from cycode.cli.exceptions.handle_auth_errors import handle_auth_exception
5+
from cycode.cli.models import CliResult
6+
from cycode.cli.printers import ConsolePrinter
7+
from cycode.cli.utils.sentry import add_breadcrumb
8+
from cycode.cyclient import logger
9+
10+
11+
def auth_command(ctx: typer.Context) -> None:
12+
"""Authenticates your machine."""
13+
add_breadcrumb('auth')
14+
15+
if ctx.invoked_subcommand is not None:
16+
# if it is a subcommand, do nothing
17+
return
18+
19+
try:
20+
logger.debug('Starting authentication process')
21+
22+
auth_manager = AuthManager()
23+
auth_manager.authenticate()
24+
25+
result = CliResult(success=True, message='Successfully logged into cycode')
26+
ConsolePrinter(ctx).print_result(result)
27+
except Exception as err:
28+
handle_auth_exception(ctx, err)
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
from typing import NamedTuple, Optional
1+
from typing import Optional
22

3-
import click
3+
import typer
44

5+
from cycode.cli.apps.auth.models import AuthInfo
56
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError
67
from cycode.cli.printers import ConsolePrinter
78
from cycode.cli.user_settings.credentials_manager import CredentialsManager
89
from cycode.cli.utils.jwt_utils import get_user_and_tenant_ids_from_access_token
910
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
1011

1112

12-
class AuthInfo(NamedTuple):
13-
user_id: str
14-
tenant_id: str
15-
16-
17-
def get_authorization_info(context: Optional[click.Context] = None) -> Optional[AuthInfo]:
13+
def get_authorization_info(ctx: Optional[typer.Context] = None) -> Optional[AuthInfo]:
1814
client_id, client_secret = CredentialsManager().get_credentials()
1915
if not client_id or not client_secret:
2016
return None
@@ -27,7 +23,7 @@ def get_authorization_info(context: Optional[click.Context] = None) -> Optional[
2723
user_id, tenant_id = get_user_and_tenant_ids_from_access_token(access_token)
2824
return AuthInfo(user_id=user_id, tenant_id=tenant_id)
2925
except (RequestHttpError, HttpUnauthorizedError):
30-
if context:
31-
ConsolePrinter(context).print_exception()
26+
if ctx:
27+
ConsolePrinter(ctx).print_exception()
3228

3329
return None

0 commit comments

Comments
 (0)