quick-status 0.6.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.
@@ -0,0 +1,7 @@
1
+ """Quick local status snapshots for developer workspaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.6.0"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,181 @@
1
+ """Data models for quick_status CI snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from quick_status.models import (
10
+ BranchState,
11
+ ChangeSummary,
12
+ CommandRecord,
13
+ RepoIdentity,
14
+ )
15
+
16
+ CI_SCHEMA_VERSION = "quick_status_ci_snapshot_v1"
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class CiGitHubStatus:
21
+ """GitHub availability facts for a CI snapshot."""
22
+
23
+ status: str
24
+ repo: str | None
25
+ error: str | None = None
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class CiPullRequest:
30
+ """Pull request facts needed for CI currentness."""
31
+
32
+ number: int
33
+ title: str
34
+ url: str
35
+ state: str
36
+ is_draft: bool
37
+ base_ref: str | None
38
+ base_oid: str | None
39
+ head_ref: str | None
40
+ head_oid: str | None
41
+ review_decision: str | None = None
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class CiCommitRefs:
46
+ """Local, PR, and expected commit facts for CI checks."""
47
+
48
+ local_head: str | None
49
+ local_short: str | None
50
+ branch: str
51
+ upstream: str | None
52
+ upstream_tracking_oid: str | None
53
+ pr_head_oid: str | None
54
+ pr_base_oid: str | None
55
+ expected_oid: str | None
56
+ expected_source: str
57
+ local_matches_pr: bool | None
58
+ local_matches_upstream_tracking: bool | None
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class CiCurrentness:
63
+ """Whether the CI evidence applies to the expected commit."""
64
+
65
+ state: str
66
+ reason: str
67
+ expected_oid: str | None
68
+ checked_oid: str | None
69
+ source: str
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class CiCheck:
74
+ """One check row from GitHub PR checks."""
75
+
76
+ name: str
77
+ workflow: str | None
78
+ status: str | None
79
+ conclusion: str | None
80
+ bucket: str
81
+ started_at: str | None
82
+ completed_at: str | None
83
+ event: str | None
84
+ url: str | None
85
+ details_url: str | None
86
+ head_sha: str | None
87
+ source: str
88
+ currentness: str
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class CiRun:
93
+ """One GitHub Actions workflow run row."""
94
+
95
+ database_id: int | None
96
+ workflow_name: str | None
97
+ display_title: str | None
98
+ event: str | None
99
+ status: str | None
100
+ conclusion: str | None
101
+ bucket: str
102
+ head_sha: str | None
103
+ head_branch: str | None
104
+ created_at: str | None
105
+ updated_at: str | None
106
+ url: str | None
107
+ attempt: int | None
108
+ currentness: str
109
+
110
+
111
+ @dataclass(frozen=True, slots=True)
112
+ class CiJob:
113
+ """One job row from a GitHub Actions workflow run."""
114
+
115
+ database_id: int | None
116
+ run_database_id: int | None
117
+ name: str
118
+ status: str | None
119
+ conclusion: str | None
120
+ bucket: str
121
+ started_at: str | None
122
+ completed_at: str | None
123
+ url: str | None
124
+
125
+
126
+ @dataclass(frozen=True, slots=True)
127
+ class CiLogTail:
128
+ """Bounded tail of failed GitHub Actions log output."""
129
+
130
+ run_database_id: int | None
131
+ status: str
132
+ requested_lines: int
133
+ capped: bool
134
+ lines: list[str] = field(default_factory=list)
135
+ reason: str | None = None
136
+
137
+
138
+ @dataclass(frozen=True, slots=True)
139
+ class CiSummary:
140
+ """Aggregate CI state for compact human output."""
141
+
142
+ ci_state: str
143
+ currentness: str
144
+ total_checks: int
145
+ pass_count: int = 0
146
+ fail_count: int = 0
147
+ pending_count: int = 0
148
+ running_count: int = 0
149
+ skipped_count: int = 0
150
+ cancel_count: int = 0
151
+ unknown_count: int = 0
152
+ failing_runs: int = 0
153
+ failing_jobs: int = 0
154
+
155
+
156
+ @dataclass(frozen=True, slots=True)
157
+ class CiSnapshot:
158
+ """Full quick_status CI snapshot."""
159
+
160
+ schema_version: str
161
+ repo: RepoIdentity
162
+ branch: BranchState
163
+ changes: ChangeSummary
164
+ github: CiGitHubStatus
165
+ pull_request: CiPullRequest | None
166
+ commits: CiCommitRefs
167
+ currentness: CiCurrentness
168
+ checks: list[CiCheck] = field(default_factory=list)
169
+ runs: list[CiRun] = field(default_factory=list)
170
+ jobs: list[CiJob] = field(default_factory=list)
171
+ log_tails: list[CiLogTail] = field(default_factory=list)
172
+ summary: CiSummary | None = None
173
+ source_errors: list[str] = field(default_factory=list)
174
+ commands: list[CommandRecord] = field(default_factory=list)
175
+
176
+ def to_dict(self, *, include_commands: bool = False) -> dict[str, object]:
177
+ """Convert the CI snapshot into stable JSON."""
178
+ payload = asdict(self)
179
+ if not include_commands:
180
+ payload.pop("commands", None)
181
+ return payload
@@ -0,0 +1,197 @@
1
+ """Human and JSON renderers for quick_status CI snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING
7
+
8
+ from quick_status import formatting as fmt
9
+
10
+ if TYPE_CHECKING:
11
+ from quick_status.ci_models import CiJob, CiRun, CiSnapshot
12
+
13
+
14
+ def render_ci_json(snapshot: CiSnapshot, *, verbose: bool = False) -> str:
15
+ """Render a CI snapshot as stable JSON."""
16
+ return json.dumps(
17
+ snapshot.to_dict(include_commands=verbose),
18
+ indent=2,
19
+ sort_keys=True,
20
+ )
21
+
22
+
23
+ def render_ci_human(
24
+ snapshot: CiSnapshot,
25
+ *,
26
+ verbose: bool = False,
27
+ color: bool = False,
28
+ ) -> str:
29
+ """Render a human-readable CI snapshot."""
30
+ lines = [
31
+ _ci_line(snapshot, color=color),
32
+ _branch_line(snapshot, color=color),
33
+ _state_line(snapshot, color=color),
34
+ _pr_line(snapshot, color=color),
35
+ _current_line(snapshot, color=color),
36
+ _summary_line(snapshot, color=color),
37
+ *_run_lines(snapshot.runs, color=color),
38
+ *_job_lines(snapshot.jobs, color=color),
39
+ *_log_tail_lines(snapshot, color=color),
40
+ ]
41
+ if snapshot.github.status != "available" and snapshot.github.error:
42
+ lines.append(
43
+ f"{fmt.label('GITHUB', color)} "
44
+ f"{fmt.state(snapshot.github.status, color)} "
45
+ f"reason={snapshot.github.error}"
46
+ )
47
+ if snapshot.source_errors:
48
+ lines.extend(
49
+ f"{fmt.label('SOURCE_ERROR', color)} {error}"
50
+ for error in snapshot.source_errors
51
+ )
52
+ if verbose:
53
+ lines.extend(fmt.command_lines(snapshot.commands, color=color))
54
+ return "\n".join(lines)
55
+
56
+
57
+ def _ci_line(snapshot: CiSnapshot, *, color: bool) -> str:
58
+ repo_name = fmt.name(snapshot.repo.name, color)
59
+ github_repo = snapshot.github.repo or snapshot.repo.github_repo or "-"
60
+ return f"{fmt.label('CI', color)} {repo_name} {github_repo}"
61
+
62
+
63
+ def _branch_line(snapshot: CiSnapshot, *, color: bool) -> str:
64
+ branch = snapshot.branch
65
+ upstream = branch.upstream or "no-upstream"
66
+ local = _short(snapshot.commits.local_head) or "-"
67
+ parts = [
68
+ f"{fmt.label('BRANCH', color)} {fmt.name(branch.head, color)}",
69
+ f"local={fmt.muted(local, color)}",
70
+ f"upstream={fmt.muted(upstream, color)}",
71
+ fmt.state(branch.sync_state, color),
72
+ ]
73
+ return " ".join(parts)
74
+
75
+
76
+ def _state_line(snapshot: CiSnapshot, *, color: bool) -> str:
77
+ changes = snapshot.changes
78
+ return (
79
+ f"{fmt.label('STATE', color)} {fmt.state(changes.worktree_state, color)} "
80
+ f"{fmt.kv('staged', changes.staged, color)} "
81
+ f"{fmt.kv('unstaged', changes.unstaged, color)} "
82
+ f"{fmt.kv('untracked', changes.untracked, color)} "
83
+ f"{fmt.kv('conflicts', changes.conflicted, color)}"
84
+ )
85
+
86
+
87
+ def _pr_line(snapshot: CiSnapshot, *, color: bool) -> str:
88
+ pull_request = snapshot.pull_request
89
+ if pull_request is None:
90
+ return f"{fmt.label('PR', color)} none"
91
+ head = _short(pull_request.head_oid) or "-"
92
+ base = pull_request.base_ref or "-"
93
+ return (
94
+ f"{fmt.label('PR', color)} #{pull_request.number} "
95
+ f"{fmt.state(pull_request.state, color)} "
96
+ f"head={fmt.muted(head, color)} base={base} url={pull_request.url}"
97
+ )
98
+
99
+
100
+ def _current_line(snapshot: CiSnapshot, *, color: bool) -> str:
101
+ currentness = snapshot.currentness
102
+ expected = _short(currentness.expected_oid) or "-"
103
+ checked = _short(currentness.checked_oid) or "-"
104
+ return (
105
+ f"{fmt.label('CURRENT', color)} {fmt.state(currentness.state, color)} "
106
+ f"expected={fmt.muted(expected, color)} checked={fmt.muted(checked, color)} "
107
+ f"source={currentness.source} reason={currentness.reason}"
108
+ )
109
+
110
+
111
+ def _summary_line(snapshot: CiSnapshot, *, color: bool) -> str:
112
+ summary = snapshot.summary
113
+ if summary is None:
114
+ return f"{fmt.label('CHECKS', color)} {fmt.state('unknown', color)}"
115
+ label = "CHECKS" if snapshot.checks else "RUNS"
116
+ return (
117
+ f"{fmt.label(label, color)} {fmt.state(summary.ci_state, color)} "
118
+ f"{fmt.kv('total', summary.total_checks, color)} "
119
+ f"{fmt.kv('pass', summary.pass_count, color)} "
120
+ f"{fmt.kv('fail', summary.fail_count, color)} "
121
+ f"{fmt.kv('pending', summary.pending_count, color)} "
122
+ f"{fmt.kv('running', summary.running_count, color)} "
123
+ f"{fmt.kv('skipped', summary.skipped_count, color)} "
124
+ f"{fmt.kv('cancel', summary.cancel_count, color)} "
125
+ f"{fmt.kv('unknown', summary.unknown_count, color)} "
126
+ f"applies_to_head={fmt.state(_applies_to_head(summary.currentness), color)}"
127
+ )
128
+
129
+
130
+ def _run_lines(runs: list[CiRun], *, color: bool) -> list[str]:
131
+ selected = runs[:5]
132
+ lines: list[str] = []
133
+ for run in selected:
134
+ name = run.workflow_name or run.display_title or "run"
135
+ run_id = run.database_id if run.database_id is not None else "-"
136
+ sha = _short(run.head_sha) or "-"
137
+ url = run.url or "-"
138
+ lines.append(
139
+ f"{fmt.label('RUN', color)} {name} "
140
+ f"{fmt.state(_display_bucket(run.bucket), color)} "
141
+ f"id={run_id} sha={fmt.muted(sha, color)} currentness={run.currentness} "
142
+ f"url={url}"
143
+ )
144
+ return lines
145
+
146
+
147
+ def _job_lines(jobs: list[CiJob], *, color: bool) -> list[str]:
148
+ return [
149
+ (
150
+ f"{fmt.label('JOB', color)} {job.name} "
151
+ f"{fmt.state(_display_bucket(job.bucket), color)} "
152
+ f"url={job.url or '-'}"
153
+ )
154
+ for job in jobs
155
+ ]
156
+
157
+
158
+ def _log_tail_lines(snapshot: CiSnapshot, *, color: bool) -> list[str]:
159
+ lines: list[str] = []
160
+ for tail in snapshot.log_tails:
161
+ if tail.status != "available":
162
+ reason = tail.reason or "unknown"
163
+ lines.append(
164
+ f"{fmt.label('LOG', color)} unavailable "
165
+ f"run={tail.run_database_id or '-'} reason={reason}"
166
+ )
167
+ continue
168
+ lines.append(
169
+ f"{fmt.label('LOG', color)} tail "
170
+ f"run={tail.run_database_id or '-'} "
171
+ f"lines={len(tail.lines)} capped={'yes' if tail.capped else 'no'}"
172
+ )
173
+ lines.extend(f" {line}" for line in tail.lines)
174
+ return lines
175
+
176
+
177
+ def _short(value: str | None) -> str | None:
178
+ if value is None:
179
+ return None
180
+ return value[:7]
181
+
182
+
183
+ def _display_bucket(bucket: str) -> str:
184
+ return {
185
+ "pass": "success",
186
+ "fail": "failure",
187
+ "cancel": "cancelled",
188
+ "skipping": "skipped",
189
+ }.get(bucket, bucket)
190
+
191
+
192
+ def _applies_to_head(currentness: str) -> str:
193
+ if currentness == "current":
194
+ return "yes"
195
+ if currentness == "stale":
196
+ return "no"
197
+ return "unknown"