cortexapps-cli 0.26.7__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. cortexapps_cli/cli.py +144 -0
  2. cortexapps_cli/command_options.py +43 -0
  3. cortexapps_cli/commands/api_keys.py +152 -0
  4. cortexapps_cli/commands/audit_logs.py +97 -0
  5. cortexapps_cli/commands/backup.py +310 -0
  6. cortexapps_cli/commands/backup_commands/cortex_export.py +174 -0
  7. cortexapps_cli/commands/catalog.py +365 -0
  8. cortexapps_cli/commands/custom_data.py +200 -0
  9. cortexapps_cli/commands/custom_events.py +248 -0
  10. cortexapps_cli/commands/custom_metrics.py +133 -0
  11. cortexapps_cli/commands/dependencies.py +232 -0
  12. cortexapps_cli/commands/deploys.py +242 -0
  13. cortexapps_cli/commands/discovery_audit.py +80 -0
  14. cortexapps_cli/commands/docs.py +63 -0
  15. cortexapps_cli/commands/entity_types.py +129 -0
  16. cortexapps_cli/commands/gitops_logs.py +75 -0
  17. cortexapps_cli/commands/groups.py +87 -0
  18. cortexapps_cli/commands/initiatives.py +121 -0
  19. cortexapps_cli/commands/integrations.py +35 -0
  20. cortexapps_cli/commands/integrations_commands/aws.py +221 -0
  21. cortexapps_cli/commands/integrations_commands/azure_devops.py +174 -0
  22. cortexapps_cli/commands/integrations_commands/azure_resources.py +258 -0
  23. cortexapps_cli/commands/integrations_commands/circleci.py +170 -0
  24. cortexapps_cli/commands/integrations_commands/coralogix.py +179 -0
  25. cortexapps_cli/commands/integrations_commands/datadog.py +178 -0
  26. cortexapps_cli/commands/integrations_commands/github.py +170 -0
  27. cortexapps_cli/commands/integrations_commands/gitlab.py +170 -0
  28. cortexapps_cli/commands/integrations_commands/incidentio.py +170 -0
  29. cortexapps_cli/commands/integrations_commands/launchdarkly.py +170 -0
  30. cortexapps_cli/commands/integrations_commands/newrelic.py +170 -0
  31. cortexapps_cli/commands/integrations_commands/pagerduty.py +170 -0
  32. cortexapps_cli/commands/integrations_commands/prometheus.py +170 -0
  33. cortexapps_cli/commands/integrations_commands/sonarqube.py +170 -0
  34. cortexapps_cli/commands/ip_allowlist.py +115 -0
  35. cortexapps_cli/commands/on_call.py +33 -0
  36. cortexapps_cli/commands/packages.py +72 -0
  37. cortexapps_cli/commands/packages_commands/go.py +37 -0
  38. cortexapps_cli/commands/packages_commands/java.py +51 -0
  39. cortexapps_cli/commands/packages_commands/node.py +65 -0
  40. cortexapps_cli/commands/packages_commands/nuget.py +50 -0
  41. cortexapps_cli/commands/packages_commands/python.py +51 -0
  42. cortexapps_cli/commands/plugins.py +153 -0
  43. cortexapps_cli/commands/queries.py +64 -0
  44. cortexapps_cli/commands/rest.py +137 -0
  45. cortexapps_cli/commands/scim.py +77 -0
  46. cortexapps_cli/commands/scorecards.py +192 -0
  47. cortexapps_cli/commands/scorecards_commands/exemptions.py +94 -0
  48. cortexapps_cli/commands/teams.py +210 -0
  49. cortexapps_cli/commands/workflows.py +144 -0
  50. cortexapps_cli/cortex.py +60 -1
  51. cortexapps_cli/cortex_client.py +178 -0
  52. cortexapps_cli/models/team.py +201 -0
  53. cortexapps_cli/utils.py +202 -0
  54. {cortexapps_cli-0.26.7.dist-info → cortexapps_cli-1.0.0.dist-info}/METADATA +29 -236
  55. cortexapps_cli-1.0.0.dist-info/RECORD +58 -0
  56. {cortexapps_cli-0.26.7.dist-info → cortexapps_cli-1.0.0.dist-info}/WHEEL +1 -1
  57. cortexapps_cli-1.0.0.dist-info/entry_points.txt +3 -0
  58. cortexapps_cli-0.26.7.dist-info/RECORD +0 -6
  59. cortexapps_cli-0.26.7.dist-info/entry_points.txt +0 -3
  60. {cortexapps_cli-0.26.7.dist-info → cortexapps_cli-1.0.0.dist-info}/LICENSE +0 -0
cortexapps_cli/cli.py ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import typer
4
+ from typing_extensions import Annotated
5
+
6
+ import os
7
+ import sys
8
+ import importlib.metadata
9
+ import tomllib
10
+ import configparser
11
+ import logging
12
+
13
+ from cortexapps_cli.cortex_client import CortexClient
14
+
15
+ import cortexapps_cli.commands.api_keys as api_keys
16
+ import cortexapps_cli.commands.audit_logs as audit_logs
17
+ import cortexapps_cli.commands.backup as backup
18
+ import cortexapps_cli.commands.catalog as catalog
19
+ import cortexapps_cli.commands.custom_data as custom_data
20
+ import cortexapps_cli.commands.custom_events as custom_events
21
+ import cortexapps_cli.commands.custom_metrics as custom_metrics
22
+ import cortexapps_cli.commands.dependencies as dependencies
23
+ import cortexapps_cli.commands.deploys as deploys
24
+ import cortexapps_cli.commands.discovery_audit as discovery_audit
25
+ import cortexapps_cli.commands.docs as docs
26
+ import cortexapps_cli.commands.entity_types as entity_types
27
+ import cortexapps_cli.commands.gitops_logs as gitops_logs
28
+ import cortexapps_cli.commands.groups as groups
29
+ import cortexapps_cli.commands.initiatives as initiatives
30
+ import cortexapps_cli.commands.integrations as integrations
31
+ import cortexapps_cli.commands.ip_allowlist as ip_allowlist
32
+ import cortexapps_cli.commands.on_call as on_call
33
+ import cortexapps_cli.commands.packages as packages
34
+ import cortexapps_cli.commands.plugins as plugins
35
+ import cortexapps_cli.commands.queries as queries
36
+ import cortexapps_cli.commands.rest as rest
37
+ import cortexapps_cli.commands.scim as scim
38
+ import cortexapps_cli.commands.scorecards as scorecards
39
+ import cortexapps_cli.commands.teams as teams
40
+ import cortexapps_cli.commands.workflows as workflows
41
+
42
+ app = typer.Typer(
43
+ no_args_is_help=True,
44
+ rich_markup_mode="rich",
45
+ context_settings={"help_option_names": ["-h", "--help"]}
46
+ )
47
+
48
+ # add subcommands
49
+ app.add_typer(api_keys.app, name="api-keys")
50
+ app.add_typer(audit_logs.app, name="audit-logs")
51
+ app.add_typer(backup.app, name="backup")
52
+ app.add_typer(catalog.app, name="catalog")
53
+ app.add_typer(custom_data.app, name="custom-data")
54
+ app.add_typer(custom_events.app, name="custom-events")
55
+ app.add_typer(custom_metrics.app, name="custom-metrics")
56
+ app.add_typer(dependencies.app, name="dependencies")
57
+ app.add_typer(deploys.app, name="deploys")
58
+ app.add_typer(discovery_audit.app, name="discovery-audit")
59
+ app.add_typer(docs.app, name="docs")
60
+ app.add_typer(entity_types.app, name="entity-types")
61
+ app.add_typer(gitops_logs.app, name="gitops-logs")
62
+ app.add_typer(groups.app, name="groups")
63
+ app.add_typer(initiatives.app, name="initiatives")
64
+ app.add_typer(integrations.app, name="integrations")
65
+ app.add_typer(ip_allowlist.app, name="ip-allowlist")
66
+ app.add_typer(on_call.app, name="on-call")
67
+ app.add_typer(packages.app, name="packages")
68
+ app.add_typer(plugins.app, name="plugins")
69
+ app.add_typer(queries.app, name="queries")
70
+ app.add_typer(rest.app, name="rest")
71
+ app.add_typer(scim.app, name="scim")
72
+ app.add_typer(scorecards.app, name="scorecards")
73
+ app.add_typer(teams.app, name="teams")
74
+ app.add_typer(workflows.app, name="workflows")
75
+
76
+ # global options
77
+ @app.callback()
78
+ def global_callback(
79
+ ctx: typer.Context,
80
+ api_key: str = typer.Option(None, "--api-key", "-k", help="API key", envvar="CORTEX_API_KEY"),
81
+ url: str = typer.Option("https://api.getcortexapp.com", "--url", "-u", help="Base URL for the API", envvar="CORTEX_BASE_URL"),
82
+ config_file: str = typer.Option(os.path.join(os.path.expanduser('~'), '.cortex', 'config'), "--config", "-c", help="Config file path", envvar="CORTEX_CONFIG"),
83
+ tenant: str = typer.Option("default", "--tenant", "-t", help="Tenant alias", envvar="CORTEX_TENANT_ALIAS"),
84
+ log_level: Annotated[str, typer.Option("--log-level", "-l", help="Set the logging level")] = "INFO"
85
+ ):
86
+ if not ctx.obj:
87
+ ctx.obj = {}
88
+
89
+ numeric_level = getattr(logging, log_level.upper(), None)
90
+ if not isinstance(numeric_level, int):
91
+ raise ValueError(f"Invalid log level: {log_level}")
92
+
93
+ if not os.path.isfile(config_file):
94
+ # no config file found
95
+ if not api_key:
96
+ raise typer.BadParameter("No API key provided and no config file found")
97
+ create_config = False
98
+
99
+ # check if we are in a terminal, if so, ask the user if they want to create a config file
100
+ if sys.stdin.isatty() and sys.stdout.isatty():
101
+ create_config = typer.confirm("No config file found. Do you want to create one?")
102
+
103
+ if create_config:
104
+ os.makedirs(os.path.dirname(config_file), exist_ok=True)
105
+ with open(config_file, "w") as f:
106
+ f.write(f"[{tenant}]\n")
107
+ f.write(f"api_key = {api_key}\n")
108
+ f.write(f"base_url = {url}\n")
109
+ else:
110
+ # config file found
111
+ # if api_key is provided, use that in preference to the config file
112
+ if not api_key:
113
+ config = configparser.ConfigParser()
114
+ config.read(config_file)
115
+ if tenant not in config:
116
+ raise typer.BadParameter(f"Tenant {tenant} not found in config file")
117
+ api_key = config[tenant]["api_key"]
118
+ if url not in config[tenant]:
119
+ url = url
120
+ else:
121
+ url = config[tenant]["base_url"]
122
+
123
+ # strip any quotes or spaces from the api_key and url
124
+ api_key = api_key.strip('"\' ')
125
+ url = url.strip('"\' /')
126
+
127
+ ctx.obj["client"] = CortexClient(api_key, tenant, numeric_level, url)
128
+
129
+ @app.command()
130
+ def version():
131
+
132
+ """
133
+ Show the version and exit.
134
+ """
135
+ try:
136
+ with open("pyproject.toml", "rb") as f:
137
+ pyproject = tomllib.load(f)
138
+ version = pyproject["tool"]["poetry"]["version"]
139
+ except Exception as e:
140
+ version = importlib.metadata.version('cortexapps_cli')
141
+ print(version)
142
+
143
+ if __name__ == "__main__":
144
+ app()
@@ -0,0 +1,43 @@
1
+ import typer
2
+ from typing import List, Optional
3
+ from typing_extensions import Annotated
4
+
5
+ class ListCommandOptions:
6
+ table_output = Annotated[
7
+ Optional[bool],
8
+ typer.Option("--table", help="Output the response as a table", show_default=False) # , callback=table_output_cb)
9
+ ]
10
+ csv_output = Annotated[
11
+ Optional[bool],
12
+ typer.Option("--csv", help="Output the response as CSV", show_default=False) # , callback=csv_output_cb)
13
+ ]
14
+ columns = Annotated[
15
+ Optional[List[str]],
16
+ typer.Option("--columns", "-C", help="Columns to include in the table, in the format HeaderName=jsonpath", show_default=False)
17
+ ]
18
+ filters = Annotated[
19
+ Optional[List[str]],
20
+ typer.Option("--filter", "-F", help="Filters to apply on rows, in the format jsonpath=regex", show_default=False)
21
+ ]
22
+ no_headers = Annotated[
23
+ Optional[bool],
24
+ typer.Option("--no-headers", help="For csv output type only: don't print header columns.", show_default=False)
25
+ ]
26
+ sort = Annotated[
27
+ Optional[List[str]],
28
+ typer.Option("--sort", "-S", help="Sort order to apply on rows, in the format jsonpath:asc or jsonpath:desc", show_default=False)
29
+ ]
30
+ page = Annotated[
31
+ Optional[int],
32
+ typer.Option("--page", "-p", help="Page number to return, 0 indexed - omit to fetch all pages", show_default=False)
33
+ ]
34
+ page_size = Annotated[
35
+ Optional[int],
36
+ typer.Option("--page-size", "-z", help="Page size for results", show_default=False)
37
+ ]
38
+
39
+ class CommandOptions:
40
+ _print = Annotated[
41
+ Optional[bool],
42
+ typer.Option("--print", help="If result should be printed to the terminal", hidden=True)
43
+ ]
@@ -0,0 +1,152 @@
1
+ from datetime import datetime
2
+ import typer
3
+ import json
4
+ from enum import Enum
5
+ from typing_extensions import Annotated
6
+ from cortexapps_cli.utils import print_output_with_context
7
+ from cortexapps_cli.command_options import CommandOptions
8
+ from cortexapps_cli.command_options import ListCommandOptions
9
+
10
+ app = typer.Typer(
11
+ help="API Keys commands",
12
+ no_args_is_help=True
13
+ )
14
+
15
+ class DefaultRole(str, Enum):
16
+ ADMIN = "ADMIN"
17
+ USER = "USER"
18
+ READ_ONLY = "READ_ONLY"
19
+
20
+ @app.command()
21
+ def list(
22
+ ctx: typer.Context,
23
+ page: ListCommandOptions.page = None,
24
+ page_size: ListCommandOptions.page_size = 250,
25
+ table_output: ListCommandOptions.table_output = False,
26
+ csv_output: ListCommandOptions.csv_output = False,
27
+ columns: ListCommandOptions.columns = [],
28
+ no_headers: ListCommandOptions.no_headers = False,
29
+ filters: ListCommandOptions.filters = [],
30
+ sort: ListCommandOptions.sort = [],
31
+ ):
32
+ """
33
+ List API keys. The API key used to make the request must have the Edit API keys permission.
34
+ """
35
+
36
+ client = ctx.obj["client"]
37
+
38
+ params = {
39
+ "page": page,
40
+ "pageSize": page_size
41
+ }
42
+
43
+ if (table_output or csv_output) and not ctx.params.get('columns'):
44
+ ctx.params['columns'] = [
45
+ "CID=cid",
46
+ "Name=name",
47
+ "Last4=last4",
48
+ "Description=description",
49
+ "Roles=roles",
50
+ "CreatedDate=createdDate",
51
+ "ExpirationDate=expirationDate",
52
+ ]
53
+
54
+ # remove any params that are None
55
+ params = {k: v for k, v in params.items() if v is not None}
56
+
57
+ if page is None:
58
+ r = client.fetch("api/v1/auth/key", params=params)
59
+ else:
60
+ r = client.get("api/v1/auth/key", params=params)
61
+ print_output_with_context(ctx, r)
62
+
63
+ @app.command()
64
+ def create(
65
+ ctx: typer.Context,
66
+ description: str | None = typer.Option(None, "--description", "-d", help="Description of the API key"),
67
+ name: str | None = typer.Option(None, "--name", "-n", help="Name of the API key"),
68
+ file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help=" File containing content; can be passed as stdin with -, example: -f-")] = None,
69
+ default_roles: str | None = typer.Option(None, "--default-roles", "-dr", help="Comma-separated list of default roles (only if file input not provided."),
70
+ custom_roles: str | None = typer.Option(None, "--custom-roles", "-cr", help="Comma-separated list of custom roles (only if file input not provided."),
71
+ expiration_date: datetime | None = typer.Option(None, "--expiration-date", "-e", help="Expiration date of the API key", formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"]),
72
+ ):
73
+ """
74
+ Create new API key. The API key used to make the request must have the Create API keys permission
75
+ """
76
+ client = ctx.obj["client"]
77
+
78
+ if file_input:
79
+ if name or description or expiration_date or default_roles or custom_roles:
80
+ raise typer.BadParameter("When providing an API definition file, do not specify any other attributes")
81
+ data = json.loads("".join([line for line in file_input]))
82
+ else:
83
+ if not default_roles and not custom_roles:
84
+ raise typer.BadParameter("One of default-roles or custom-roles is required")
85
+
86
+ data = {
87
+ "roles": [],
88
+ "name": name
89
+ }
90
+
91
+ if default_roles is not None:
92
+ for role in default_roles.split(","):
93
+ data["roles"].append({"role": role, "type": "DEFAULT"})
94
+ if custom_roles is not None:
95
+ for role in custom_roles.split(","):
96
+ data["roles"].append({"tag": role, "type": "CUSTOM"})
97
+
98
+ if description:
99
+ data["description"] = description
100
+ if expiration_date:
101
+ data["expirationDate"] = expiration_date.strftime('%Y-%m-%dT%H:%M:%S.000Z')
102
+
103
+ r = client.post("api/v1/auth/key", data=data)
104
+ print_output_with_context(ctx, r)
105
+
106
+ @app.command()
107
+ def update(
108
+ ctx: typer.Context,
109
+ cid: str = typer.Option(..., "--cid", "-c", help="The unique, auto-generated identifier for the API key"),
110
+ description: str | None = typer.Option(None, "--description", "-d", help="Description of the API key"),
111
+ name: str = typer.Option(..., "--name", "-n", help="Name of the API key"),
112
+ ):
113
+ """
114
+ Update API key. The API key used to make the request must have the Edit API keys permission.
115
+ """
116
+ client = ctx.obj["client"]
117
+
118
+ data = {
119
+ "name": name
120
+ }
121
+ if description is not None:
122
+ data["description"] = description
123
+
124
+ r = client.put("api/v1/auth/key/" + cid, data=data)
125
+ print_output_with_context(ctx, r)
126
+
127
+ @app.command()
128
+ def get(
129
+ ctx: typer.Context,
130
+ cid: str = typer.Option(..., "--cid", "-c", help="The unique, auto-generated identifier for the API key"),
131
+ ):
132
+ """
133
+ Get API key.
134
+ """
135
+
136
+ client = ctx.obj["client"]
137
+
138
+ r = client.get("api/v1/auth/key/"+ cid)
139
+ print_output_with_context(ctx, r)
140
+
141
+ @app.command()
142
+ def delete(
143
+ ctx: typer.Context,
144
+ cid: str = typer.Option(..., "--cid", "-c", help="The unique, auto-generated identifier for the API key"),
145
+ ):
146
+ """
147
+ Delete API key.
148
+ """
149
+
150
+ client = ctx.obj["client"]
151
+
152
+ r = client.delete("api/v1/auth/key/"+ cid)
@@ -0,0 +1,97 @@
1
+ from datetime import datetime
2
+ from enum import Enum
3
+ import typer
4
+ from cortexapps_cli.command_options import ListCommandOptions
5
+ from cortexapps_cli.utils import print_output_with_context, print_output
6
+
7
+ app = typer.Typer(
8
+ help="Audit log commands",
9
+ no_args_is_help=True
10
+ )
11
+
12
+ class Action(str, Enum):
13
+ CREATE = "CREATE"
14
+ DELETE = "DELETE"
15
+ UPDATE = "UPDATE"
16
+
17
+ class ActorType(str, Enum):
18
+ ANONYMOUS = "ANONYMOUS"
19
+ API_KEY = "API_KEY"
20
+ BACKSTAGE = "BACKSTAGE"
21
+ OAUTH2 = "OAUTH2"
22
+ PERSONAL_API_KEY = "PERSONAL_API_KEY"
23
+
24
+ class ActorRequestType(str, Enum):
25
+ API_KEY_ENTITY = "API_KEY_ENTITY"
26
+ ATLASSIAN_WEBHOOK = "ATLASSIAN_WEBHOOK"
27
+ SCORECARD_BADGES = "SCORECARD_BADGES"
28
+ SLACK_COMMAND = "SLACK_COMMAND"
29
+
30
+ @app.command()
31
+ def get(
32
+ ctx: typer.Context,
33
+ actions: list[Action] | None = typer.Option(None, "--actions", "-a", help="The audit action"),
34
+ actorApiKeyIdentifiers: list[str] | None = typer.Option(None, "--actorApiKeyIdentifiers", "-ak", help="API key name associated with audit event"),
35
+ actorEmails: list[str] | None = typer.Option(None, "--actorEmails", "-ae", help="Email address associated with audit event"),
36
+ actorIpAddresses: list[str] | None = typer.Option(None, "--actorIpAddresses", "-ai", help="Source IP Addresses associated with audit event"),
37
+ actorRequestTypes: list[ActorRequestType] | None = typer.Option(None, "--actorRequestTypes", "-ar", help="Request event associated with audit event"),
38
+ actorTypes: list[ActorType] | None = typer.Option(None, "--actorTypes", "-at", help="Actor that triggered the audit event"),
39
+ end_time: datetime = typer.Option(None, "--end-time", "-e", help="End time of audit logs to retrieve", formats=["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"]),
40
+ objectIdentifiers: list[str] | None = typer.Option(None, "--objectIdentifiers", "-oi", help="The name of the Cortex object that was modified, ie x-cortex-tag value, metadata field name, etc."),
41
+ objectTypes: list[str] | None = typer.Option(None, "--objectTypes", "-ot", help="ObjectTypes"),
42
+ start_time: datetime = typer.Option(None, "--start-time", "-s", help="Start time of audit logs to retrieve", formats=["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"]),
43
+ page: ListCommandOptions.page = None,
44
+ page_size: ListCommandOptions.page_size = 250,
45
+ table_output: ListCommandOptions.table_output = False,
46
+ csv_output: ListCommandOptions.csv_output = False,
47
+ columns: ListCommandOptions.columns = [],
48
+ no_headers: ListCommandOptions.no_headers = False,
49
+ filters: ListCommandOptions.filters = [],
50
+ sort: ListCommandOptions.sort = [],
51
+ ):
52
+ """
53
+ Note: To see the complete list of possible values, please reference the available filter options for audit logs under Settings in the app.
54
+ """
55
+ client = ctx.obj["client"]
56
+
57
+ params = {
58
+ "actions": actions,
59
+ "actorApiKeyIdentifiers": actorApiKeyIdentifiers,
60
+ "actorEmails": actorEmails,
61
+ "actorIpAddresses": actorIpAddresses,
62
+ "actorRequestTypes": actorRequestTypes,
63
+ "actorTypes": actorTypes,
64
+ "endTime": end_time,
65
+ "objectIdentifiers": objectIdentifiers,
66
+ "objectTypes": objectTypes,
67
+ "page": page,
68
+ "pageSize": page_size,
69
+ "startTime": start_time
70
+ }
71
+
72
+ # remove any params that are None
73
+ params = {k: v for k, v in params.items() if v is not None}
74
+
75
+ # convert datetime and list types to string
76
+ for k, v in params.items():
77
+ if str(type(v)) == "<class 'datetime.datetime'>":
78
+ params[k] = v.strftime('%Y-%m-%dT%H:%M:%S')
79
+ if str(type(v)) == "<class 'list'>":
80
+ params[k] = ','.join(v)
81
+
82
+ if (table_output or csv_output) and not ctx.params.get('columns'):
83
+ ctx.params['columns'] = [
84
+ "Action=action",
85
+ "ObjectType=objectType",
86
+ "ActorIdentifier=actorIdentifier",
87
+ "ObjectIdentifier=objectIdentifier",
88
+ "IpAddress=ipAddress",
89
+ "Timestamp=timestamp",
90
+ ]
91
+
92
+ if page is None:
93
+ r = client.fetch("api/v1/audit-logs", params=params)
94
+ else:
95
+ r = client.get("api/v1/audit-logs", params=params)
96
+
97
+ print_output_with_context(ctx, r)