From 2588e228ade5a93d2c37a7c109c4f5c251dfb15a Mon Sep 17 00:00:00 2001 From: Kunwar Sahni Date: Mon, 21 Feb 2022 18:09:07 -0500 Subject: [PATCH] Release/3.3.0 (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 emit correct compiler for C files when generating CDB (#188) * Update Azure Pipelines with new self hosted M1 Mac (#189) * Update azure-pipelines.yml for Azure Pipelines * Update azure-pipelines.yml * Update azure-pipelines.yml for Azure Pipelines * Update azure-pipelines.yml for Azure Pipelines * Fixes 'make.exe.exe' typo. Adds disable sentry prompt option for VSCode. (#190) * Fixes 'make.exe.exe' typo. Adds disable sentry prompt option for VSCode. * Fix sentry-off flag * change sentry-off to no-sentry * Update Version (#186) (#191) * ✨Add Analytics (#193) * Fix and move no-sentry to default options. Setup framework for analytics * Fix some things. --toggle-analytics works * Analytics should be working. Need to figure out which commands matter * Make use-analytics a choice not a toggle * Show no-analytics flag feedback so user knows it works * Analytics appear to be working! * Reset uid to None * Compress code a bit * Remove interactive command analytics. Fix info-project typo * Move GA config to cli.pros. Fixes --use-analytics * ✨More Upload Options (#194) * Start of more upload options * Adds project icon, name, and description. Use name/description="string" * pros v5 rm-program command * rm program literals * Remove extra print messages. Kernel version = None when no project * Update Version (#186) (#195) * Update Version to 3.3.0 * 🐛Use Correct Linked Remote Name for Upload (#199) * ✨Add Icon and After support to upload_options (#200) * Change no internet exception to warning. Co-authored-by: Alex Brooke Co-authored-by: BennyBot <48661356+BennyBot@users.noreply.github.com> Co-authored-by: Benjamin Davis --- azure-pipelines.yml | 22 +++++++++ pip_version | 2 +- pros/cli/build.py | 5 ++ pros/cli/common.py | 35 +++++++++++++- pros/cli/conductor.py | 12 +++-- pros/cli/conductor_utils.py | 4 +- pros/cli/interactive.py | 2 +- pros/cli/main.py | 25 +++++++++- pros/cli/misc_commands.py | 3 +- pros/cli/terminal.py | 3 +- pros/cli/upload.py | 40 +++++++++++++--- pros/cli/user_script.py | 3 +- pros/cli/v5_utils.py | 30 +++++++++++- pros/common/sentry.py | 10 ++-- pros/conductor/project/__init__.py | 6 +-- pros/config/cli_config.py | 2 +- pros/ga/analytics.py | 72 ++++++++++++++++++++++++++++ pros/serial/devices/vex/v5_device.py | 9 ++-- version | 2 +- win_version | 2 +- 20 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 pros/ga/analytics.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c2d087be..edcaf60b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -63,6 +63,28 @@ jobs: artifactName: 'pros_cli-$(Build.BuildNumber)-macos-$(buildArch)' targetPath: 'out' +- job: macOSM1 + timeoutInMinutes: 30 + dependsOn: UpdateBuildNumber + strategy: + maxParallel: 2 + matrix: + arm64: + buildArch: arm64 + pool: + M1 Mac + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(python.version) + architecture: $(buildArch) + - bash: scripts/build.sh + displayName: Build + - task: PublishPipelineArtifact@0 + inputs: + artifactName: 'pros_cli-$(Build.BuildNumber)-macos-$(buildArch)' + targetPath: 'out' + - job: Linux timeoutInMinutes: 30 dependsOn: UpdateBuildNumber diff --git a/pip_version b/pip_version index 06eda28a..0fa4ae48 100644 --- a/pip_version +++ b/pip_version @@ -1 +1 @@ -3.2.3 \ No newline at end of file +3.3.0 \ No newline at end of file diff --git a/pros/cli/build.py b/pros/cli/build.py index 248a7db3..2e9c6ec7 100644 --- a/pros/cli/build.py +++ b/pros/cli/build.py @@ -3,6 +3,7 @@ import click import pros.conductor as c +from pros.ga.analytics import analytics from pros.cli.common import default_options, logger, project_option, pros_root, shadow_command from .upload import upload @@ -20,6 +21,7 @@ def make(project: c.Project, build_args): """ Build current PROS project or cwd """ + analytics.send("make") exit_code = project.compile(build_args) if exit_code != 0: logger(__name__).error(f'Failed to make project: Exit Code {exit_code}', extra={'sentry': False}) @@ -33,6 +35,7 @@ def make(project: c.Project, build_args): @project_option() @click.pass_context def make_upload(ctx, project: c.Project, build_args: List[str], **upload_args): + analytics.send("make-upload") ctx.invoke(make, project=project, build_args=build_args) ctx.invoke(upload, project=project, **upload_args) @@ -43,6 +46,7 @@ def make_upload(ctx, project: c.Project, build_args: List[str], **upload_args): @project_option() @click.pass_context def make_upload_terminal(ctx, project: c.Project, build_args, **upload_args): + analytics.send("make-upload-terminal") from .terminal import terminal ctx.invoke(make, project=project, build_args=build_args) ctx.invoke(upload, project=project, **upload_args) @@ -63,6 +67,7 @@ def build_compile_commands(project: c.Project, suppress_output: bool, compile_co Build a compile_commands.json compatible with cquery :return: """ + analytics.send("build-compile-commands") exit_code = project.make_scan_build(build_args, cdb_file=compile_commands, suppress_output=suppress_output, sandbox=sandbox) if exit_code != 0: diff --git a/pros/cli/common.py b/pros/cli/common.py index d883b4a4..e666877d 100644 --- a/pros/cli/common.py +++ b/pros/cli/common.py @@ -1,7 +1,9 @@ import click.core from pros.common.sentry import add_tag +from pros.ga.analytics import analytics from pros.common.utils import * +from pros.common.ui import echo from .click_classes import * @@ -113,12 +115,41 @@ def callback(ctx: click.Context, param: click.Parameter, value: str): decorator.__name__ = f.__name__ return decorator +def no_sentry_option(f: Union[click.Command, Callable]): + """ + disables the sentry y/N prompt when an error/exception occurs + """ + def callback(ctx: click.Context, param: click.Parameter, value: bool): + ctx.ensure_object(dict) + add_tag('no-sentry',value) + if value: + pros.common.sentry.disable_prompt() + decorator = click.option('--no-sentry', expose_value=False, is_flag=True, default=False, is_eager=True, + help="Disable sentry reporting prompt.", callback=callback, cls=PROSOption, hidden=True)(f) + decorator.__name__ = f.__name__ + return decorator + +def no_analytics(f: Union[click.Command, Callable]): + """ + Don't use analytics for this command + """ + def callback(ctx: click.Context, param: click.Parameter, value: bool): + ctx.ensure_object(dict) + add_tag('no-analytics',value) + if value: + echo("Not sending analytics for this command.\n") + analytics.useAnalytics = False + pass + decorator = click.option('--no-analytics', expose_value=False, is_flag=True, default=False, is_eager=True, + help="Don't send analytics for this command.", callback=callback, cls=PROSOption, hidden=True)(f) + decorator.__name__ = f.__name__ + return decorator def default_options(f: Union[click.Command, Callable]): """ - combines verbosity, debug, machine output options (most commonly used) + combines verbosity, debug, machine output, no analytics, and no sentry options """ - decorator = debug_option(verbose_option(logging_option(logfile_option(machine_output_option(f))))) + decorator = debug_option(verbose_option(logging_option(logfile_option(machine_output_option(no_sentry_option(no_analytics(f))))))) decorator.__name__ = f.__name__ return decorator diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index 1a1d2f66..e1d89565 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -5,7 +5,7 @@ import pros.conductor as c from pros.cli.common import * from pros.conductor.templates import ExternalTemplate - +from pros.ga.analytics import analytics @pros_root def conductor_cli(): @@ -41,7 +41,7 @@ def fetch(query: c.BaseTemplate): Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ - + analytics.send("fetch-template") template_file = None if os.path.exists(query.identifier): template_file = query.identifier @@ -98,6 +98,7 @@ def apply(project: c.Project, query: c.BaseTemplate, **kwargs): Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ + analytics.send("apply-template") return c.Conductor().apply_template(project, identifier=query, **kwargs) @@ -122,6 +123,7 @@ def install(ctx: click.Context, **kwargs): Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ + analytics.send("install-template") return ctx.invoke(apply, install_ok=True, **kwargs) @@ -146,6 +148,7 @@ def upgrade(ctx: click.Context, project: c.Project, query: c.BaseTemplate, **kwa Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ + analytics.send("upgrade-project") if not query.name: for template in project.templates.keys(): click.secho(f'Upgrading {template}', color='yellow') @@ -170,6 +173,7 @@ def uninstall_template(project: c.Project, query: c.BaseTemplate, remove_user: b Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ + analytics.send("uninstall-template") c.Conductor().remove_template(project, query, remove_user=remove_user, remove_empty_directories=remove_empty_directories) @@ -200,6 +204,7 @@ def new_project(ctx: click.Context, path: str, target: str, version: str, Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ + analytics.send("new-project") if version.lower() == 'latest' or not version: version = '>0' if not force_system and c.Project.find_project(path) is not None: @@ -246,6 +251,7 @@ def query_templates(ctx, query: c.BaseTemplate, allow_offline: bool, allow_onlin Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ + analytics.send("query-templates") if limit < 0: limit = 15 templates = c.Conductor().resolve_templates(query, allow_offline=allow_offline, allow_online=allow_online, @@ -285,7 +291,7 @@ def info_project(project: c.Project, ls_upgrades): Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ - + analytics.send("info-project") from pros.conductor.project import ProjectReport report = ProjectReport(project) _conductor = c.Conductor() diff --git a/pros/cli/conductor_utils.py b/pros/cli/conductor_utils.py index 3de7d346..20a52fcd 100644 --- a/pros/cli/conductor_utils.py +++ b/pros/cli/conductor_utils.py @@ -10,7 +10,7 @@ import pros.conductor as c from pros.common.utils import logger from pros.conductor.templates import ExternalTemplate - +from pros.ga.analytics import analytics from .common import default_options, template_query from .conductor import conductor @@ -55,6 +55,7 @@ def create_template(ctx, path: str, destination: str, do_zip: bool, **kwargs): pros conduct create-template . libblrs 2.0.1 --system "firmware/*.a" --system "include/*.h" """ + analytics.send("create-template") project = c.Project.find_project(path, recurse_times=1) if project: project = c.Project(project) @@ -151,6 +152,7 @@ def filename_remap(file_path: str) -> str: @template_query(required=False) @default_options def purge_template(query: c.BaseTemplate, force): + analytics.send("purge-template") if not query: force = click.confirm('Are you sure you want to remove all cached templates? This action is non-reversable!', abort=True) diff --git a/pros/cli/interactive.py b/pros/cli/interactive.py index cbf33ac7..634f1b2f 100644 --- a/pros/cli/interactive.py +++ b/pros/cli/interactive.py @@ -3,7 +3,7 @@ import click import pros.conductor as c from .common import PROSGroup, default_options, project_option, pros_root - +from pros.ga.analytics import analytics @pros_root def interactive_cli(): diff --git a/pros/cli/main.py b/pros/cli/main.py index eb6d1c0f..d35c4aef 100644 --- a/pros/cli/main.py +++ b/pros/cli/main.py @@ -1,4 +1,7 @@ import logging + +# Setup analytics first because it is used by other files + import os.path import pros.common.sentry @@ -11,6 +14,8 @@ from pros.cli.click_classes import * from pros.cli.common import default_options, root_commands from pros.common.utils import get_version, logger +from pros.ga.analytics import analytics + root_sources = [ 'build', @@ -61,16 +66,32 @@ def version(ctx: click.Context, param, value): ui.echo('pros, version {}'.format(get_version())) ctx.exit(0) +def use_analytics(ctx: click.Context, param, value): + if value==None: + return + touse = not analytics.useAnalytics + if str(value).lower().startswith("t"): + touse = True + elif str(value).lower().startswith("f"): + touse = False + else: + ui.echo('Invalid argument provided for \'--use-analytics\'. Try \'--use-analytics=False\' or \'--use-analytics=True\'') + ctx.exit(0) + ctx.ensure_object(dict) + analytics.set_use(touse) + ui.echo('Analytics set to : {}'.format(analytics.useAnalytics)) + ctx.exit(0) @click.command('pros', cls=PROSCommandCollection, sources=root_commands) @default_options -@click.option('--version', help='Displays version and exits', is_flag=True, expose_value=False, is_eager=True, +@click.option('--version', help='Displays version and exits.', is_flag=True, expose_value=False, is_eager=True, callback=version) +@click.option('--use-analytics', help='Set analytics usage (True/False).', type=str, expose_value=False, + is_eager=True, default=None, callback=use_analytics) def cli(): pros.common.sentry.register() - if __name__ == '__main__': main() diff --git a/pros/cli/misc_commands.py b/pros/cli/misc_commands.py index db1bf4a4..56c63217 100644 --- a/pros/cli/misc_commands.py +++ b/pros/cli/misc_commands.py @@ -1,6 +1,6 @@ import pros.common.ui as ui from pros.cli.common import * - +from pros.ga.analytics import analytics @pros_root def misc_commands_cli(): @@ -17,6 +17,7 @@ def upgrade(force_check, no_install): """ Check for updates to the PROS CLI """ + analytics.send("upgrade") from pros.upgrade import UpgradeManager manager = UpgradeManager() manifest = manager.get_manifest(force_check) diff --git a/pros/cli/terminal.py b/pros/cli/terminal.py index 6c68b5aa..b31bf898 100644 --- a/pros/cli/terminal.py +++ b/pros/cli/terminal.py @@ -10,7 +10,7 @@ from pros.common.utils import logger from .common import default_options, resolve_v5_port, resolve_cortex_port, pros_root from pros.serial.ports.v5_wireless_port import V5WirelessPort - +from pros.ga.analytics import analytics @pros_root def terminal_cli(): @@ -40,6 +40,7 @@ def terminal(port: str, backend: str, **kwargs): Note: share backend is not yet implemented. """ + analytics.send("terminal") from pros.serial.devices.vex.v5_user_device import V5UserDevice from pros.serial.terminal import Terminal is_v5_user_joystick = False diff --git a/pros/cli/upload.py b/pros/cli/upload.py index 894741f7..63f132a4 100644 --- a/pros/cli/upload.py +++ b/pros/cli/upload.py @@ -1,10 +1,11 @@ from sys import exit +from unicodedata import name import pros.common.ui as ui import pros.conductor as c from .common import * - +from pros.ga.analytics import analytics @pros_root def upload_cli(): @@ -28,14 +29,19 @@ def upload_cli(): cls=PROSOption, group='V5 Options') @click.option('--slot', default=None, type=click.IntRange(min=1, max=8), help='Program slot on the GUI.', cls=PROSOption, group='V5 Options') +@click.option('--icon', type=click.Choice(['pros','pizza','planet','alien','ufo','robot','clawbot','question','X','power']), default='pros', + help="Change Program's icon on the V5 Brain", cls=PROSOption, group='V5 Options') @click.option('--program-version', default=None, type=str, help='Specify version metadata for program.', cls=PROSOption, group='V5 Options', hidden=True) -@click.option('--icon', default=None, type=str, - cls=PROSOption, group='V5 Options', hidden=True) @click.option('--ini-config', type=click.Path(exists=True), default=None, help='Specify a program configuration file.', cls=PROSOption, group='V5 Options', hidden=True) @click.option('--compress-bin/--no-compress-bin', 'compress_bin', cls=PROSOption, group='V5 Options', default=True, help='Compress the program binary before uploading.') +@click.option('--description', default="Made with PROS", type=str, cls=PROSOption, group='V5 Options', + help='Change the description displayed for the program.') +@click.option('--name', default=None, type=str, cls=PROSOption, group='V5 Options', + help='Change the name of the program.') + @default_options def upload(path: Optional[str], project: Optional[c.Project], port: str, **kwargs): """ @@ -47,8 +53,11 @@ def upload(path: Optional[str], project: Optional[c.Project], port: str, **kwarg [PORT] may be any valid communication port file, such as COM1 or /dev/ttyACM0. If left blank, then a port is automatically detected based on the target (or as supplied by the PROS project) """ + analytics.send("upload") import pros.serial.devices.vex as vex from pros.serial.ports import DirectPort + kwargs['ide_version'] = project.kernel if not project==None else "None" + kwargs['ide'] = 'PROS' if path is None or os.path.isdir(path): if project is None: project_path = c.Project.find_project(path or os.getcwd()) @@ -65,19 +74,34 @@ def upload(path: Optional[str], project: Optional[c.Project], port: str, **kwarg kwargs.pop('slot') elif kwargs.get('slot', None) is None: kwargs['slot'] = 1 + if 'icon' in options and kwargs.get('icon','pros') == 'pros': + kwargs.pop('icon') + if 'after' in options and kwargs.get('after','screen') is None: + kwargs.pop('after') + options.update(kwargs) kwargs = options - kwargs['target'] = project.target # enforce target because uploading to the wrong uC is VERY bad if 'program-version' in kwargs: kwargs['version'] = kwargs['program-version'] if 'remote_name' not in kwargs: kwargs['remote_name'] = project.name - + name_to_file = { + 'pros' : 'USER902x.bmp', + 'pizza' : 'USER003x.bmp', + 'planet' : 'USER013x.bmp', + 'alien' : 'USER027x.bmp', + 'ufo' : 'USER029x.bmp', + 'clawbot' : 'USER010x.bmp', + 'robot' : 'USER011x.bmp', + 'question' : 'USER002x.bmp', + 'power' : 'USER012x.bmp', + 'X' : 'USER001x.bmp' + } + kwargs['icon'] = name_to_file[kwargs['icon']] if 'target' not in kwargs or kwargs['target'] is None: logger(__name__).debug(f'Target not specified. Arguments provided: {kwargs}') raise click.UsageError('Target not specified. specify a project (using the file argument) or target manually') - if kwargs['target'] == 'v5': port = resolve_v5_port(port, 'system')[0] elif kwargs['target'] == 'cortex': @@ -87,8 +111,8 @@ def upload(path: Optional[str], project: Optional[c.Project], port: str, **kwarg logger(__name__).debug('Target should be one of ("v5" or "cortex").') if not port: raise dont_send(click.UsageError('No port provided or located. Make sure to specify --target if needed.')) - if kwargs['target'] == 'v5': + kwargs['remote_name'] = kwargs['name'] if kwargs.get("name",None) else kwargs['remote_name'] if kwargs['remote_name'] is None: kwargs['remote_name'] = os.path.splitext(os.path.basename(path))[0] kwargs['remote_name'] = kwargs['remote_name'].replace('@', '_') @@ -138,6 +162,7 @@ def ls_usb(target): """ List plugged in VEX Devices """ + analytics.send("ls-usb") from pros.serial.devices.vex import find_v5_ports, find_cortex_ports class PortReport(object): @@ -177,6 +202,7 @@ def __str__(self): @shadow_command(upload) @click.pass_context def make_upload_terminal(ctx, **upload_kwargs): + analytics.send("upload-terminal") from .terminal import terminal ctx.invoke(upload, **upload_kwargs) ctx.invoke(terminal, request_banner=False) diff --git a/pros/cli/user_script.py b/pros/cli/user_script.py index e6112a82..be0f8259 100644 --- a/pros/cli/user_script.py +++ b/pros/cli/user_script.py @@ -2,7 +2,7 @@ from pros.common import ui from .common import default_options, pros_root - +from pros.ga.analytics import analytics @pros_root def user_script_cli(): @@ -16,6 +16,7 @@ def user_script(script_file): """ Run a script file with the PROS CLI package """ + analytics.send("user-script") import os.path import importlib.util package_name = os.path.splitext(os.path.split(script_file)[0])[0] diff --git a/pros/cli/v5_utils.py b/pros/cli/v5_utils.py index ec35ebb8..6e9565ef 100644 --- a/pros/cli/v5_utils.py +++ b/pros/cli/v5_utils.py @@ -1,5 +1,5 @@ from .common import * - +from pros.ga.analytics import analytics @pros_root def v5_utils_cli(): @@ -19,6 +19,7 @@ def status(port: str): """ Print system information for the V5 """ + analytics.send("status") from pros.serial.devices.vex import V5Device from pros.serial.ports import DirectPort port = resolve_v5_port(port, 'system')[0] @@ -46,6 +47,7 @@ def ls_files(port: str, vid: int, options: int): """ List files on the flash filesystem """ + analytics.send("ls-files") from pros.serial.devices.vex import V5Device from pros.serial.ports import DirectPort port = resolve_v5_port(port, 'system')[0] @@ -70,6 +72,7 @@ def read_file(file_name: str, port: str, vid: int, source: str): """ Read file on the flash filesystem to stdout """ + analytics.send("read-file") from pros.serial.devices.vex import V5Device from pros.serial.ports import DirectPort port = resolve_v5_port(port, 'system')[0] @@ -95,6 +98,7 @@ def write_file(file, port: str, remote_file: str, **kwargs): """ Write a file to the V5. """ + analytics.send("write-file") from pros.serial.ports import DirectPort from pros.serial.devices.vex import V5Device port = resolve_v5_port(port, 'system')[0] @@ -117,6 +121,7 @@ def rm_file(file_name: str, port: str, vid: int, erase_all: bool): """ Remove a file from the flash filesystem """ + analytics.send("rm-file") from pros.serial.devices.vex import V5Device from pros.serial.ports import DirectPort port = resolve_v5_port(port, 'system')[0] @@ -137,6 +142,7 @@ def cat_metadata(file_name: str, port: str, vid: int): """ Print metadata for a file """ + analytics.send("cat-metadata") from pros.serial.devices.vex import V5Device from pros.serial.ports import DirectPort port = resolve_v5_port(port, 'system')[0] @@ -147,6 +153,26 @@ def cat_metadata(file_name: str, port: str, vid: int): device = V5Device(ser) print(device.get_file_metadata_by_name(file_name, vid=vid)) +@v5.command('rm-program') +@click.argument('slot') +@click.argument('port', type=int, required=False, default=None) +@click.option('--vid', type=int, default=1, cls=PROSOption, hidden=True) +@default_options +def rm_program(slot: int, port: str, vid: int): + """ + Remove a program from the flash filesystem + """ + from pros.serial.devices.vex import V5Device + from pros.serial.ports import DirectPort + port = resolve_v5_port(port, 'system')[0] + if not port: + return - 1 + + base_name = f'slot_{slot}' + ser = DirectPort(port) + device = V5Device(ser) + device.erase_file(f'{base_name}.ini', vid=vid) + device.erase_file(f'{base_name}.bin', vid=vid) @v5.command('rm-all') @click.argument('port', required=False, default=None) @@ -156,6 +182,7 @@ def rm_all(port: str, vid: int): """ Remove all user programs from the V5 """ + analytics.send("rm-all") from pros.serial.devices.vex import V5Device from pros.serial.ports import DirectPort port = resolve_v5_port(port, 'system')[0] @@ -180,6 +207,7 @@ def run(slot: str, port: str): """ Run a V5 program """ + analytics.send("run") from pros.serial.devices.vex import V5Device from pros.serial.ports import DirectPort file = f'slot_{slot}.bin' diff --git a/pros/common/sentry.py b/pros/common/sentry.py index 67c07491..6c0c8690 100644 --- a/pros/common/sentry.py +++ b/pros/common/sentry.py @@ -10,9 +10,12 @@ from pros.config.cli_config import CliConfig # noqa: F401, flake8 issue, flake8 issue with "if TYPE_CHECKING" cli_config: 'CliConfig' = None - +force_prompt_off = False SUPPRESSED_EXCEPTIONS = [PermissionError, click.Abort] +def disable_prompt(): + global force_prompt_off + force_prompt_off = True def prompt_to_send(event: Dict[str, Any], hint: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: """ @@ -22,7 +25,9 @@ def prompt_to_send(event: Dict[str, Any], hint: Optional[Dict[str, Any]]) -> Opt with ui.Notification(): if cli_config is None or (cli_config.offer_sentry is not None and not cli_config.offer_sentry): return - + if force_prompt_off: + ui.logger(__name__).debug('Sentry prompt was forced off through click option') + return if 'extra' in event and not event['extra'].get('sentry', True): ui.logger(__name__).debug('Not sending candidate event because event was tagged with extra.sentry = False') return @@ -109,7 +114,6 @@ def add_tag(key: str, value: str): def register(cfg: Optional['CliConfig'] = None): global cli_config, client - if cfg is None: from pros.config.cli_config import cli_config as get_cli_config cli_config = get_cli_config() diff --git a/pros/conductor/project/__init__.py b/pros/conductor/project/__init__.py index a0d41a6f..798fdc3c 100644 --- a/pros/conductor/project/__init__.py +++ b/pros/conductor/project/__init__.py @@ -233,7 +233,7 @@ def make(self, build_args: List[str]): except Exception as e: if not os.environ.get('PROS_TOOLCHAIN'): ui.logger(__name__).warn("PROS toolchain not found! Please ensure the toolchain is installed correctly and your environment variables are set properly.\n") - ui.logger(__name__).error(f"ERROR WHILE CALLING '{make_cmd}.exe' WITH EXCEPTION: {str(e)}\n") + ui.logger(__name__).error(f"ERROR WHILE CALLING '{make_cmd}' WITH EXCEPTION: {str(e)}\n",extra={'sentry':False}) stdout_pipe.close() stderr_pipe.close() sys.exit() @@ -290,7 +290,7 @@ def libscanbuild_capture(args: argparse.Namespace) -> Tuple[int, Iterable[Compil except Exception as e: if not os.environ.get('PROS_TOOLCHAIN'): ui.logger(__name__).warn("PROS toolchain not found! Please ensure the toolchain is installed correctly and your environment variables are set properly.\n") - ui.logger(__name__).error(f"ERROR WHILE CALLING '{make_cmd}.exe' WITH EXCEPTION: {str(e)}\n") + ui.logger(__name__).error(f"ERROR WHILE CALLING '{make_cmd}' WITH EXCEPTION: {str(e)}\n",extra={'sentry':False}) if not suppress_output: pipe.close() sys.exit() @@ -385,7 +385,7 @@ def new_entry_map(entry): def entry_map(entry: Compilation): json_entry = entry.as_db_entry() - json_entry['arguments'][0] = 'clang' if entry.compiler == 'cc' else 'clang++' + json_entry['arguments'][0] = 'clang' if entry.compiler == 'c' else 'clang++' return json_entry entries = itertools.chain(old_entries, new_entries) diff --git a/pros/config/cli_config.py b/pros/config/cli_config.py index 1f134383..8c962047 100644 --- a/pros/config/cli_config.py +++ b/pros/config/cli_config.py @@ -20,7 +20,7 @@ def __init__(self, file=None): self.update_frequency: timedelta = timedelta(hours=1) self.override_use_build_compile_commands: Optional[bool] = None self.offer_sentry: Optional[bool] = None - + self.ga: Optional[dict] = None super(CliConfig, self).__init__(file) def needs_online_fetch(self, last_fetch: datetime) -> bool: diff --git a/pros/ga/analytics.py b/pros/ga/analytics.py new file mode 100644 index 00000000..f838dec0 --- /dev/null +++ b/pros/ga/analytics.py @@ -0,0 +1,72 @@ +import json +from os import path +import uuid +import requests +import random + +url = 'https://www.google-analytics.com/collect' +agent = 'pros-cli' + +""" +PROS ANALYTICS CLASS +""" + +class Analytics(): + def __init__(self): + from pros.config.cli_config import cli_config as get_cli_config + self.cli_config = get_cli_config() + #If GA hasn't been setup yet (first time install/update) + if not self.cli_config.ga: + #Default values for GA + self.cli_config.ga = { + "enabled": "True", + "ga_id": "UA-84548828-8", + "u_id": str(uuid.uuid4()) + } + self.cli_config.save() + self.sent = False + #Variables that the class will use + self.gaID = self.cli_config.ga['ga_id'] + self.useAnalytics = self.cli_config.ga['enabled'] + self.uID = self.cli_config.ga['u_id'] + + def send(self,action): + if not self.useAnalytics or self.sent: + return + self.sent=True # Prevent Send from being called multiple times + try: + #Payload to be sent to GA, idk what some of them are but it works + payload = { + 'v': 1, + 'tid': self.gaID, + 'aip': 1, + 'z': random.random(), + 'cid': self.uID, + 't': 'event', + 'ec': 'action', + 'ea': action, + 'el': 'CLI', + 'ev': '1', + 'ni': 0 + } + + #Send payload to GA servers + response = requests.post(url=url, + data=payload, + headers={'User-Agent': agent}, + timeout=5.0) + if not response.status_code==200: + print("Something went wrong while sending analytics!") + print(response) + return response + except Exception as e: + from pros.cli.common import logger + logger(__name__).warning("Unable to send analytics. Do you have a stable internet connection?", extra={'sentry': False}) + + def set_use(self, value: bool): + #Sets if GA is being used or not + self.useAnalytics = value + self.cli_config.ga['enabled'] = self.useAnalytics + self.cli_config.save() + +analytics = Analytics() \ No newline at end of file diff --git a/pros/serial/devices/vex/v5_device.py b/pros/serial/devices/vex/v5_device.py index bee6c9e7..c65861e5 100644 --- a/pros/serial/devices/vex/v5_device.py +++ b/pros/serial/devices/vex/v5_device.py @@ -270,6 +270,10 @@ def generate_ini_file(self, remote_name: str = None, slot: int = 0, ini: ConfigP project_ini = ConfigParser() from semantic_version import Spec default_icon = 'USER902x.bmp' if Spec('>=1.0.0-22').match(self.status['cpu0_version']) else 'USER999x.bmp' + project_ini['project'] = { + 'version': str(kwargs.get('ide_version') or get_version()), + 'ide': str(kwargs.get('ide') or 'PROS') + } project_ini['program'] = { 'version': kwargs.get('version', '0.0.0') or '0.0.0', 'name': remote_name, @@ -295,13 +299,12 @@ def write_program(self, file: typing.BinaryIO, remote_name: str = None, ini: Con action_string = f'Uploading program "{remote_name}"' finish_string = f'Finished uploading "{remote_name}"' if hasattr(file, 'name'): - action_string += f' ({Path(file.name).name})' - finish_string += f' ({Path(file.name).name})' + action_string += f' ({remote_name if remote_name else Path(file.name).name})' + finish_string += f' ({remote_name if remote_name else Path(file.name).name})' action_string += f' to V5 slot {slot + 1} on {self.port}' if compress_bin: action_string += ' (compressed)' ui.echo(action_string) - remote_base = f'slot_{slot + 1}' if target == 'ddr': self.write_file(file, f'{remote_base}.bin', file_len=file_len, type='bin', diff --git a/version b/version index 06eda28a..0fa4ae48 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.2.3 \ No newline at end of file +3.3.0 \ No newline at end of file diff --git a/win_version b/win_version index 901a7931..b59e4e59 100644 --- a/win_version +++ b/win_version @@ -1 +1 @@ -3.2.3.0 \ No newline at end of file +3.3.0.0 \ No newline at end of file