kctl-sentry 0.6.3__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.
@@ -0,0 +1,3 @@
1
+ """kctl-sentry: Sentry error tracking management."""
2
+
3
+ __version__ = "0.6.3"
@@ -0,0 +1,5 @@
1
+ """Allow running as python -m kctl_sentry."""
2
+
3
+ from kctl_sentry.cli import _run
4
+
5
+ _run()
kctl_sentry/cli.py ADDED
@@ -0,0 +1,136 @@
1
+ """Main CLI entry point for kctl-sentry."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from kctl_lib import KctlError, handle_cli_error
10
+
11
+ from kctl_sentry import __version__
12
+ from kctl_sentry.commands.alerts import app as alerts_app
13
+ from kctl_sentry.commands.config_cmd import app as config_app
14
+ from kctl_sentry.commands.dashboard import app as dashboard_app
15
+ from kctl_sentry.commands.environments import app as environments_app
16
+ from kctl_sentry.commands.health import app as health_app
17
+ from kctl_sentry.commands.issues import app as issues_app
18
+ from kctl_sentry.commands.projects import app as projects_app
19
+ from kctl_sentry.commands.releases import app as releases_app
20
+ from kctl_sentry.commands.stats import app as stats_app
21
+ from kctl_sentry.commands.teams import app as teams_app
22
+ from kctl_sentry.core.callbacks import AppContext
23
+ from kctl_sentry.core.plugins import discover_and_load_plugins
24
+ from kctl_sentry.commands.skill_cmd import app as skill_app
25
+ from kctl_sentry.commands.doctor_cmd import app as doctor_app
26
+ from kctl_lib.self_update import notify_if_outdated
27
+ from kctl_lib.tui import add_tui_command
28
+
29
+
30
+ def version_callback(value: bool) -> None:
31
+ if value:
32
+ typer.echo(f"kctl-sentry {__version__}")
33
+ raise typer.Exit()
34
+
35
+
36
+ app = typer.Typer(
37
+ name="kctl-sentry",
38
+ help="Kodemeio Sentry CLI — error triage, release tracking, and project management.",
39
+ no_args_is_help=True,
40
+ rich_markup_mode="rich",
41
+ pretty_exceptions_enable=False,
42
+ )
43
+
44
+
45
+ @app.callback()
46
+ def main(
47
+ ctx: typer.Context,
48
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
49
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
50
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
51
+ format: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")] = "pretty",
52
+ no_header: Annotated[bool, typer.Option("--no-header", help="Omit header row in CSV output")] = False,
53
+ auth_token: Annotated[str | None, typer.Option("--auth-token", help="Auth token override")] = None,
54
+ version: Annotated[
55
+ bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
56
+ ] = False,
57
+ ) -> None:
58
+ """Kodemeio Sentry CLI."""
59
+ ctx.ensure_object(dict)
60
+ ctx.obj = AppContext(
61
+ json_mode=json_output,
62
+ quiet=quiet,
63
+ profile=profile,
64
+ format=format,
65
+ no_header=no_header,
66
+ auth_token_override=auth_token,
67
+ )
68
+ notify_if_outdated(ctx.obj.output, "kctl-sentry", __version__)
69
+
70
+
71
+ app.add_typer(config_app, name="config")
72
+ app.add_typer(health_app, name="health")
73
+ app.add_typer(dashboard_app, name="dashboard")
74
+ app.add_typer(issues_app, name="issues")
75
+ app.add_typer(projects_app, name="projects")
76
+ app.add_typer(releases_app, name="releases")
77
+ app.add_typer(alerts_app, name="alerts")
78
+ app.add_typer(stats_app, name="stats")
79
+ app.add_typer(teams_app, name="teams")
80
+ app.add_typer(environments_app, name="environments")
81
+ app.add_typer(skill_app, name="skill", hidden=True)
82
+ app.add_typer(doctor_app, name="doctor")
83
+
84
+ # Load third-party plugins via entry points
85
+ discover_and_load_plugins(app)
86
+ add_tui_command(app, service_key="sentry", version=__version__)
87
+
88
+
89
+ @app.command("self-update")
90
+ def self_update_cmd(ctx: typer.Context) -> None:
91
+ """Check for updates and upgrade kctl-sentry."""
92
+ actx = ctx.obj
93
+ out = actx.output
94
+
95
+ from kctl_lib.self_update import check_update
96
+ from kctl_lib.self_update import update as do_update
97
+
98
+ latest = check_update("kctl-sentry", __version__)
99
+ if latest:
100
+ out.info(f"Updating to {latest}...")
101
+ do_update("kctl-sentry")
102
+ out.success(f"Updated to {latest}")
103
+ else:
104
+ out.success("Already up to date")
105
+
106
+
107
+ @app.command()
108
+ def completions(
109
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
110
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
111
+ ) -> None:
112
+ """Generate or install shell completions."""
113
+ from kctl_lib.completions import get_completion_script, install_completions
114
+
115
+ if install:
116
+ path = install_completions("kctl-sentry", shell)
117
+ if path:
118
+ typer.echo(f"Completions installed to {path}")
119
+ else:
120
+ typer.echo(f"Could not install completions for {shell}", err=True)
121
+ raise typer.Exit(code=1)
122
+ else:
123
+ script = get_completion_script("kctl-sentry", shell)
124
+ typer.echo(script)
125
+
126
+
127
+ def _run() -> None:
128
+ """Entry point with error handling."""
129
+ try:
130
+ app()
131
+ except KctlError as e:
132
+ handle_cli_error(e)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ _run()
File without changes
@@ -0,0 +1,176 @@
1
+ """Alert rule management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_sentry.core.callbacks import AppContext
10
+ from kctl_sentry.core.exceptions import KctlError
11
+
12
+ app = typer.Typer(help="Manage alert rules.")
13
+
14
+
15
+ @app.command("list")
16
+ def list_(
17
+ ctx: typer.Context,
18
+ project: Annotated[str | None, typer.Option("--project", "-p", help="Filter by project slug")] = None,
19
+ ) -> None:
20
+ """List alert rules."""
21
+ c: AppContext = ctx.obj
22
+ out = c.output
23
+
24
+ try:
25
+ rules = c.client.project_get(project, "/rules/") if project else c.client.org_get("/alert-rules/")
26
+ if not isinstance(rules, list):
27
+ rules = []
28
+
29
+ rows: list[list[str]] = []
30
+ for rule in rules:
31
+ rule_id = str(rule.get("id", ""))
32
+ name = (rule.get("name", "") or "")[:50]
33
+ # Project-scoped rules have 'projects', org-scoped have 'project'
34
+ proj_info = ""
35
+ if isinstance(rule.get("projects"), list):
36
+ proj_info = ", ".join(rule["projects"])
37
+ elif isinstance(rule.get("project"), dict):
38
+ proj_info = rule["project"].get("slug", "")
39
+
40
+ status_val = rule.get("status", "active")
41
+ owner = ""
42
+ if isinstance(rule.get("owner"), dict):
43
+ owner = rule["owner"].get("name", "")
44
+ elif isinstance(rule.get("owner"), str):
45
+ owner = rule["owner"]
46
+
47
+ rows.append([rule_id, name, proj_info, status_val, owner])
48
+
49
+ out.table(
50
+ "Alert Rules",
51
+ [
52
+ ("ID", "cyan"),
53
+ ("Name", ""),
54
+ ("Project", ""),
55
+ ("Status", "green"),
56
+ ("Owner", "dim"),
57
+ ],
58
+ rows,
59
+ data_for_json=rules,
60
+ )
61
+ except KctlError as e:
62
+ out.error(f"Failed to list alerts: {e}")
63
+ raise typer.Exit(1) from e
64
+
65
+
66
+ @app.command("show")
67
+ def show(
68
+ ctx: typer.Context,
69
+ rule_id: Annotated[str, typer.Argument(help="Alert rule ID")],
70
+ project: Annotated[str, typer.Option("--project", "-p", help="Project slug")],
71
+ ) -> None:
72
+ """Show alert rule details and trigger history."""
73
+ c: AppContext = ctx.obj
74
+ out = c.output
75
+
76
+ try:
77
+ rule = c.client.project_get(project, f"/rules/{rule_id}/")
78
+ if not isinstance(rule, dict):
79
+ rule = {}
80
+
81
+ # Parse conditions
82
+ conditions = rule.get("conditions", [])
83
+ condition_strs: list[tuple[str, str]] = []
84
+ for cond in conditions if isinstance(conditions, list) else []:
85
+ if isinstance(cond, dict):
86
+ condition_strs.append(("Condition", cond.get("name", str(cond.get("id", "")))))
87
+
88
+ # Parse actions
89
+ actions = rule.get("actions", [])
90
+ action_strs: list[tuple[str, str]] = []
91
+ for act in actions if isinstance(actions, list) else []:
92
+ if isinstance(act, dict):
93
+ action_strs.append(("Action", act.get("name", str(act.get("id", "")))))
94
+
95
+ sections = [
96
+ (
97
+ "Alert Rule",
98
+ [
99
+ ("ID", str(rule.get("id", ""))),
100
+ ("Name", rule.get("name", "")),
101
+ ("Status", rule.get("status", "")),
102
+ ("Frequency", f"{rule.get('frequency', '')} seconds"),
103
+ ("Date created", (rule.get("dateCreated", "") or "")[:19]),
104
+ ],
105
+ ),
106
+ ]
107
+
108
+ if condition_strs:
109
+ sections.append(("Conditions", condition_strs))
110
+ if action_strs:
111
+ sections.append(("Actions", action_strs))
112
+
113
+ out.detail(
114
+ f"Alert Rule: {rule.get('name', rule_id)}",
115
+ sections,
116
+ data_for_json=rule,
117
+ )
118
+ except KctlError as e:
119
+ out.error(f"Failed to show alert: {e}")
120
+ raise typer.Exit(1) from e
121
+
122
+
123
+ @app.command("create")
124
+ def create(
125
+ ctx: typer.Context,
126
+ project: Annotated[str, typer.Option("--project", "-p", help="Project slug")],
127
+ name: Annotated[str, typer.Option("--name", "-n", help="Alert rule name")],
128
+ metric: Annotated[str, typer.Option("--metric", help="Metric: events, users")] = "events",
129
+ threshold: Annotated[int, typer.Option("--threshold", help="Threshold value")] = 100,
130
+ time_window: Annotated[int, typer.Option("--time-window", help="Time window in minutes")] = 60,
131
+ ) -> None:
132
+ """Create a new metric alert rule."""
133
+ c: AppContext = ctx.obj
134
+ out = c.output
135
+
136
+ try:
137
+ # Map metric to Sentry's aggregate format
138
+ aggregate_map = {
139
+ "events": "count()",
140
+ "users": "count_unique(user)",
141
+ }
142
+ aggregate = aggregate_map.get(metric, "count()")
143
+
144
+ payload = {
145
+ "name": name,
146
+ "aggregate": aggregate,
147
+ "timeWindow": time_window,
148
+ "dataset": "events",
149
+ "query": "",
150
+ "thresholdType": 0, # Above threshold
151
+ "resolveThreshold": None,
152
+ "triggers": [
153
+ {
154
+ "label": "critical",
155
+ "alertThreshold": threshold,
156
+ "actions": [],
157
+ }
158
+ ],
159
+ "projects": [project],
160
+ "owner": None,
161
+ }
162
+
163
+ result = c.client.org_post("/alert-rules/", json=payload)
164
+ if not isinstance(result, dict):
165
+ result = {}
166
+
167
+ if out.json_mode:
168
+ out.raw_json(result)
169
+ else:
170
+ out.success(f"Alert rule created: {result.get('name', name)}")
171
+ out.kv("ID", str(result.get("id", "")))
172
+ out.kv("Metric", aggregate)
173
+ out.kv("Threshold", str(threshold))
174
+ except KctlError as e:
175
+ out.error(f"Failed to create alert: {e}")
176
+ raise typer.Exit(1) from e