avr-cli 0.2.0__tar.gz → 0.3.0__tar.gz

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 (94) hide show
  1. {avr_cli-0.2.0 → avr_cli-0.3.0}/PKG-INFO +1 -1
  2. {avr_cli-0.2.0 → avr_cli-0.3.0}/avr_cli.egg-info/PKG-INFO +1 -1
  3. {avr_cli-0.2.0 → avr_cli-0.3.0}/avr_cli.egg-info/SOURCES.txt +9 -0
  4. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/_build_info.py +1 -1
  5. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/auth.py +9 -1
  6. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/job.py +9 -3
  7. avr_cli-0.3.0/avrea_cli/commands/pr.py +338 -0
  8. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/repo.py +373 -1
  9. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/run.py +203 -52
  10. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/display.py +5 -0
  11. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/helpers.py +61 -3
  12. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/log_display.py +9 -1
  13. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/main.py +9 -1
  14. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/repo_context.py +2 -2
  15. avr_cli-0.3.0/avrea_cli/run_diagnostics.py +651 -0
  16. avr_cli-0.3.0/avrea_cli/run_diagnostics_display.py +172 -0
  17. avr_cli-0.3.0/avrea_cli/run_refs.py +242 -0
  18. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/vm.py +156 -8
  19. {avr_cli-0.2.0 → avr_cli-0.3.0}/pyproject.toml +1 -1
  20. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_auth_login.py +63 -0
  21. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_display.py +8 -1
  22. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_helpers.py +99 -0
  23. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_job_view_logs.py +31 -0
  24. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_piped_output.py +75 -3
  25. avr_cli-0.3.0/tests/test_pr_list.py +297 -0
  26. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_repo_context.py +26 -3
  27. avr_cli-0.3.0/tests/test_repo_mirror.py +310 -0
  28. avr_cli-0.3.0/tests/test_run_command_refs.py +122 -0
  29. avr_cli-0.3.0/tests/test_run_diagnose.py +348 -0
  30. avr_cli-0.3.0/tests/test_run_refs.py +174 -0
  31. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_run_view.py +120 -0
  32. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_vm.py +116 -0
  33. {avr_cli-0.2.0 → avr_cli-0.3.0}/LICENSE +0 -0
  34. {avr_cli-0.2.0 → avr_cli-0.3.0}/README.md +0 -0
  35. {avr_cli-0.2.0 → avr_cli-0.3.0}/avr_cli.egg-info/dependency_links.txt +0 -0
  36. {avr_cli-0.2.0 → avr_cli-0.3.0}/avr_cli.egg-info/entry_points.txt +0 -0
  37. {avr_cli-0.2.0 → avr_cli-0.3.0}/avr_cli.egg-info/requires.txt +0 -0
  38. {avr_cli-0.2.0 → avr_cli-0.3.0}/avr_cli.egg-info/top_level.txt +0 -0
  39. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/__init__.py +0 -0
  40. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/api_client.py +0 -0
  41. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/assets/logo_mark.svg +0 -0
  42. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/audit.py +0 -0
  43. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/billing.py +0 -0
  44. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/click_ext.py +0 -0
  45. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/__init__.py +0 -0
  46. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/auth_cmd.py +0 -0
  47. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/cache.py +0 -0
  48. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/config_cmd.py +0 -0
  49. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/health.py +0 -0
  50. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/internal.py +0 -0
  51. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/log.py +0 -0
  52. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/org.py +0 -0
  53. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/settings.py +0 -0
  54. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/status.py +0 -0
  55. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/commands/workflow.py +0 -0
  56. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/config.py +0 -0
  57. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/docs_gen.py +0 -0
  58. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/firewall.py +0 -0
  59. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/json_output.py +0 -0
  60. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/metrics_display.py +0 -0
  61. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/output.py +0 -0
  62. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/paths.py +0 -0
  63. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/transport.py +0 -0
  64. {avr_cli-0.2.0 → avr_cli-0.3.0}/avrea_cli/version.py +0 -0
  65. {avr_cli-0.2.0 → avr_cli-0.3.0}/setup.cfg +0 -0
  66. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_api_client.py +0 -0
  67. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_auth.py +0 -0
  68. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_auth_sso.py +0 -0
  69. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_cache.py +0 -0
  70. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_config.py +0 -0
  71. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_config_cmd.py +0 -0
  72. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_docs_gen.py +0 -0
  73. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_env_conventions.py +0 -0
  74. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_firewall.py +0 -0
  75. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_global_flags.py +0 -0
  76. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_job_list.py +0 -0
  77. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_job_metrics_cmd.py +0 -0
  78. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_json_output.py +0 -0
  79. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_lazy_registry.py +0 -0
  80. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_log_display.py +0 -0
  81. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_metrics_display.py +0 -0
  82. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_org_email_domain.py +0 -0
  83. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_org_saml.py +0 -0
  84. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_output.py +0 -0
  85. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_public_mirrors.py +0 -0
  86. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_run_attempts.py +0 -0
  87. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_run_list_flags.py +0 -0
  88. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_run_logs.py +0 -0
  89. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_settings.py +0 -0
  90. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_transport.py +0 -0
  91. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_web_flag.py +0 -0
  92. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_workflow_actions.py +0 -0
  93. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_workflow_run.py +0 -0
  94. {avr_cli-0.2.0 → avr_cli-0.3.0}/tests/test_workflow_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avr-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Avrea command-line client
5
5
  Author-email: Avrea <support@avrea.com>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avr-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Avrea command-line client
5
5
  Author-email: Avrea <support@avrea.com>
6
6
  License-Expression: Apache-2.0
@@ -26,6 +26,9 @@ avrea_cli/metrics_display.py
26
26
  avrea_cli/output.py
27
27
  avrea_cli/paths.py
28
28
  avrea_cli/repo_context.py
29
+ avrea_cli/run_diagnostics.py
30
+ avrea_cli/run_diagnostics_display.py
31
+ avrea_cli/run_refs.py
29
32
  avrea_cli/transport.py
30
33
  avrea_cli/version.py
31
34
  avrea_cli/vm.py
@@ -39,6 +42,7 @@ avrea_cli/commands/internal.py
39
42
  avrea_cli/commands/job.py
40
43
  avrea_cli/commands/log.py
41
44
  avrea_cli/commands/org.py
45
+ avrea_cli/commands/pr.py
42
46
  avrea_cli/commands/repo.py
43
47
  avrea_cli/commands/run.py
44
48
  avrea_cli/commands/settings.py
@@ -68,11 +72,16 @@ tests/test_org_email_domain.py
68
72
  tests/test_org_saml.py
69
73
  tests/test_output.py
70
74
  tests/test_piped_output.py
75
+ tests/test_pr_list.py
71
76
  tests/test_public_mirrors.py
72
77
  tests/test_repo_context.py
78
+ tests/test_repo_mirror.py
73
79
  tests/test_run_attempts.py
80
+ tests/test_run_command_refs.py
81
+ tests/test_run_diagnose.py
74
82
  tests/test_run_list_flags.py
75
83
  tests/test_run_logs.py
84
+ tests/test_run_refs.py
76
85
  tests/test_run_view.py
77
86
  tests/test_settings.py
78
87
  tests/test_transport.py
@@ -1,2 +1,2 @@
1
1
  # Auto-generated by .github/workflows/release.yml; do not edit.
2
- GIT_COMMIT = "0ee3d6ede53b90032c4a40561338c3e1b03d2c2b"
2
+ GIT_COMMIT = "98e501799b97fcf00fda142d41d8dce6befa8a3e"
@@ -515,6 +515,14 @@ def login(public_api_url: str, *, provider: str = "github", email: str | None =
515
515
  pass # Best-effort; the session will expire on its own
516
516
 
517
517
  return api_key
518
+ except httpx.HTTPStatusError as e:
519
+ # Not handle_http_error: this is the login flow itself, where a 401
520
+ # means the browser session expired, not "go run `avr auth login`".
521
+ click.echo(
522
+ f"Error: {public_api_url} returned HTTP {e.response.status_code} when creating an API key.",
523
+ err=True,
524
+ )
525
+ raise click.Abort() from None
518
526
  except httpx.HTTPError as e:
519
- click.echo(f"Error: Failed to create API key: {e}", err=True)
527
+ click.echo(f"Error: Could not reach {public_api_url} to create an API key: {e}", err=True)
520
528
  raise click.Abort() from None
@@ -186,7 +186,10 @@ def job(ctx):
186
186
  "--repo",
187
187
  "repository_ids",
188
188
  multiple=True,
189
- help="Filter by repository (org/repo or rep-xxx, repeatable). Auto-detected from git remote if omitted.",
189
+ help=(
190
+ "Filter by repository (org/repo or rep-xxx). Pass --repo more than once to filter multiple repositories. "
191
+ "Auto-detected from git remote if omitted."
192
+ ),
190
193
  )
191
194
  @click.option("--name", "job_names", multiple=True, help="Filter by job name (repeatable).")
192
195
  @click.option(
@@ -300,7 +303,7 @@ def job_list(
300
303
  link_slug = get_org_slug(client, org_id) if links_enabled else ""
301
304
  _print_jobs_table(jobs_data, console_url=link_console_url, slug=link_slug)
302
305
 
303
- next_cursor = response.get("pagination", {}).get("next_cursor")
306
+ next_cursor = (response.get("pagination") or {}).get("next_cursor")
304
307
  if next_cursor:
305
308
  click.echo(f"\nMore results available. Next page: --cursor {next_cursor}", err=True)
306
309
 
@@ -311,7 +314,10 @@ def job_list(
311
314
  "--repo",
312
315
  "repository_ids",
313
316
  multiple=True,
314
- help="Filter by repository (org/repo or rep-xxx, repeatable). Auto-detected from git remote if omitted.",
317
+ help=(
318
+ "Filter by repository (org/repo or rep-xxx). Pass --repo more than once to filter multiple repositories. "
319
+ "Auto-detected from git remote if omitted."
320
+ ),
315
321
  )
316
322
  @click.option("--name", "job_names", multiple=True, help="Filter by job name (repeatable).")
317
323
  @click.option("--interval", type=int, default=5, show_default=True, help="Refresh interval in seconds.")
@@ -0,0 +1,338 @@
1
+ """Pull request CLI commands."""
2
+
3
+ from avrea_cli.api_client import ApiClient
4
+ from avrea_cli.click_ext import GhGroup
5
+ from avrea_cli.config import CliConfig
6
+ from avrea_cli.display import DIM_FG
7
+ from avrea_cli.display import get_console_url
8
+ from avrea_cli.display import hyperlink
9
+ from avrea_cli.display import is_piped
10
+ from avrea_cli.display import pr_url
11
+ from avrea_cli.display import print_piped_header
12
+ from avrea_cli.display import print_piped_row
13
+ from avrea_cli.display import truncate
14
+ from avrea_cli.helpers import ensure_authenticated
15
+ from avrea_cli.helpers import ensure_ctx
16
+ from avrea_cli.helpers import get_org_id
17
+ from avrea_cli.helpers import get_org_slug
18
+ from avrea_cli.helpers import handle_http_error
19
+ from avrea_cli.helpers import validate_cursor
20
+ from avrea_cli.json_output import emit_json
21
+ from avrea_cli.json_output import handle_json_meta
22
+ from avrea_cli.json_output import make_schema
23
+ from avrea_cli.json_output import split_fields
24
+ from avrea_cli.output import format_relative_timestamp
25
+ from avrea_cli.repo_context import resolve_repos_or_detect
26
+ from typing import Any
27
+ import click
28
+ import httpx
29
+
30
+ _PR_LIST_FIELDS = make_schema(
31
+ "number",
32
+ "title",
33
+ "state",
34
+ "draft",
35
+ "merged",
36
+ "author_login",
37
+ "base_ref",
38
+ "head_ref",
39
+ "head_sha",
40
+ "base_sha",
41
+ "created_at",
42
+ "updated_at",
43
+ "comment_count",
44
+ "unresolved_thread_count",
45
+ "check_status",
46
+ "mergeability",
47
+ "repository_id",
48
+ "repository_full_name",
49
+ )
50
+
51
+ _SCOPES = ["all", "authored", "involved"]
52
+ _STATES = ["open", "closed", "merged", "all"]
53
+ _PR_LIST_FEATURE_FLAG = "feature.org-pull-requests.enabled"
54
+ _PR_LIST_UNSUPPORTED_MESSAGE = "Sorry, pull request listing is not yet supported for this organization. Coming soon."
55
+ _PR_LIST_AVAILABILITY_UNKNOWN_MESSAGE = (
56
+ "Sorry, we could not find this organization or confirm pull request listing support. "
57
+ "Organizations without Avrea Git are not supported yet. Coming soon."
58
+ )
59
+
60
+
61
+ @click.group(cls=GhGroup)
62
+ @click.pass_context
63
+ def pr(ctx):
64
+ """View pull requests."""
65
+ ensure_ctx(ctx)
66
+
67
+
68
+ @pr.command("list")
69
+ @click.option(
70
+ "--org", "org_id", help="Organization ID or slug. Uses default org if not specified (see: avr config set org)."
71
+ )
72
+ @click.option(
73
+ "--repo",
74
+ "repository_ids",
75
+ multiple=True,
76
+ help=(
77
+ "Filter by repository (org/repo or rep-xxx). Pass --repo more than once to filter multiple repositories. "
78
+ "Auto-detected from git remote if omitted."
79
+ ),
80
+ )
81
+ @click.option(
82
+ "--scope",
83
+ type=click.Choice(_SCOPES, case_sensitive=False),
84
+ default="all",
85
+ show_default=True,
86
+ help="List every readable PR, PRs you authored, or PRs you are involved in.",
87
+ )
88
+ @click.option(
89
+ "--state",
90
+ type=click.Choice(_STATES, case_sensitive=False),
91
+ default="open",
92
+ show_default=True,
93
+ help="Filter by pull request state. 'all' removes the state filter.",
94
+ )
95
+ @click.option("-L", "--limit", type=click.IntRange(1, 200), default=20, show_default=True, help="Max PRs to return.")
96
+ @click.option("--cursor", default=None, help="Pagination cursor from a previous response.")
97
+ @click.option(
98
+ "--json",
99
+ "json_fields",
100
+ default=None,
101
+ help='Output JSON. Pass comma-separated field names, "*" for all fields, or "?" to list available fields.',
102
+ )
103
+ @click.option("-q", "--jq", "jq_expr", default=None, help="Filter --json output through a jq expression.")
104
+ @click.pass_context
105
+ def pr_list(
106
+ ctx,
107
+ org_id,
108
+ repository_ids,
109
+ scope,
110
+ state,
111
+ limit,
112
+ cursor,
113
+ json_fields,
114
+ jq_expr,
115
+ ):
116
+ """List pull requests across repositories.
117
+
118
+ \b
119
+ Examples:
120
+ avr pr list
121
+ avr pr list --scope authored
122
+ avr pr list --repo acme/widgets --state merged
123
+ avr pr list --json number,title,mergeability
124
+ avr pr list --json '?' # list available fields
125
+ avr pr list --json '*' # all fields
126
+
127
+ \b
128
+ JSON FIELDS
129
+ author_login, base_ref, base_sha, check_status, comment_count, created_at,
130
+ draft, head_ref, head_sha, mergeability, merged, number, repository_full_name,
131
+ repository_id, state, title, unresolved_thread_count, updated_at
132
+ """
133
+ if handle_json_meta(json_fields, jq_expr, _PR_LIST_FIELDS):
134
+ return
135
+
136
+ client: ApiClient = ctx.obj["client"]
137
+ config: CliConfig = ctx.obj["config"]
138
+ ensure_authenticated(config)
139
+ org_id = get_org_id(config, org_id, client=client)
140
+
141
+ try:
142
+ feature = client.public_get(f"/orgs/{org_id}/feature-flags/{_PR_LIST_FEATURE_FLAG}")
143
+ except httpx.HTTPStatusError as exc:
144
+ if exc.response.status_code == 404:
145
+ raise click.ClickException(_PR_LIST_AVAILABILITY_UNKNOWN_MESSAGE) from None
146
+ handle_http_error(exc, "check pull request listing availability")
147
+ except httpx.TransportError as exc:
148
+ raise click.ClickException(f"Could not list pull requests because the request to Avrea failed: {exc}") from None
149
+
150
+ if feature.get("enabled") is not True:
151
+ raise click.ClickException(_PR_LIST_UNSUPPORTED_MESSAGE)
152
+
153
+ resolved_repositories = resolve_repos_or_detect(
154
+ client,
155
+ config,
156
+ org_id,
157
+ repository_ids,
158
+ soft_detect=True,
159
+ )
160
+
161
+ params: dict[str, Any] = {
162
+ "scope": scope.lower(),
163
+ "limit": limit,
164
+ }
165
+ if state.lower() != "all":
166
+ params["state"] = state.lower()
167
+ if resolved_repositories:
168
+ params["repository_ids"] = resolved_repositories
169
+ cursor = validate_cursor(cursor)
170
+ if cursor:
171
+ params["cursor"] = cursor
172
+
173
+ try:
174
+ response = client.public_get(f"/orgs/{org_id}/pull-requests", params=params)
175
+ pulls = response.get("data") or []
176
+ except httpx.HTTPStatusError as exc:
177
+ handle_http_error(exc, "list pull requests")
178
+ except httpx.TransportError as exc:
179
+ raise click.ClickException(f"Could not list pull requests because the request to Avrea failed: {exc}") from None
180
+
181
+ if json_fields is not None:
182
+ emit_json(list(pulls), split_fields(json_fields, _PR_LIST_FIELDS), _PR_LIST_FIELDS, jq_expr)
183
+ return
184
+
185
+ if not pulls:
186
+ click.echo("No pull requests found.")
187
+ return
188
+
189
+ if is_piped():
190
+ _print_piped(pulls)
191
+ else:
192
+ _print_table(
193
+ ctx,
194
+ client,
195
+ config,
196
+ org_id,
197
+ pulls,
198
+ show_repository=len(resolved_repositories) != 1,
199
+ )
200
+
201
+ next_cursor = (response.get("pagination") or {}).get("next_cursor")
202
+ if next_cursor:
203
+ click.echo(f"\nMore results available. Next page: --cursor {next_cursor}", err=True)
204
+
205
+
206
+ def _display_state(pull: dict[str, Any]) -> str:
207
+ if pull.get("merged"):
208
+ return "merged"
209
+ if pull.get("draft") and pull.get("state") != "closed":
210
+ return "draft"
211
+ return str(pull.get("state") or "unknown")
212
+
213
+
214
+ def _merge_status(pull: dict[str, Any]) -> str:
215
+ mergeability = pull.get("mergeability")
216
+ if isinstance(mergeability, dict) and mergeability.get("status"):
217
+ return str(mergeability["status"])
218
+ if pull.get("merged") or pull.get("draft") or pull.get("state") == "closed":
219
+ return "-"
220
+ return "?"
221
+
222
+
223
+ def _print_piped(pulls: list[dict[str, Any]]) -> None:
224
+ print_piped_header(
225
+ [
226
+ "number",
227
+ "repository",
228
+ "state",
229
+ "title",
230
+ "author",
231
+ "head_ref",
232
+ "base_ref",
233
+ "check_status",
234
+ "mergeability",
235
+ "comment_count",
236
+ "unresolved_threads",
237
+ "head_sha",
238
+ "updated_at",
239
+ ]
240
+ )
241
+ for pull in pulls:
242
+ print_piped_row(
243
+ [
244
+ pull.get("number"),
245
+ pull.get("repository_full_name") or pull.get("repository_id"),
246
+ _display_state(pull),
247
+ pull.get("title"),
248
+ pull.get("author_login"),
249
+ pull.get("head_ref"),
250
+ pull.get("base_ref"),
251
+ pull.get("check_status"),
252
+ (pull.get("mergeability") or {}).get("status"),
253
+ pull.get("comment_count"),
254
+ pull.get("unresolved_thread_count"),
255
+ pull.get("head_sha"),
256
+ pull.get("updated_at"),
257
+ ]
258
+ )
259
+
260
+
261
+ def _print_table(
262
+ ctx: click.Context,
263
+ client: ApiClient,
264
+ config: CliConfig,
265
+ org_id: str,
266
+ pulls: list[dict[str, Any]],
267
+ *,
268
+ show_repository: bool,
269
+ ) -> None:
270
+ widths = {
271
+ "repo": 30,
272
+ "pr": 8,
273
+ "state": 8,
274
+ "checks": 11,
275
+ "merge": 12,
276
+ "unres": 7,
277
+ "title": 48,
278
+ "author": 18,
279
+ "age": 10,
280
+ }
281
+
282
+ def cell(value: object, width: int) -> str:
283
+ return f"{truncate(str(value), width - 1):{width}s}"
284
+
285
+ def header(value: str, width: int) -> str:
286
+ return click.style(cell(value, width), fg=DIM_FG, underline=True)
287
+
288
+ click.echo(
289
+ " "
290
+ + header("PR", widths["pr"])
291
+ + (header("REPOSITORY", widths["repo"]) if show_repository else "")
292
+ + header("STATE", widths["state"])
293
+ + header("CHECKS", widths["checks"])
294
+ + header("MERGE", widths["merge"])
295
+ + header("UNRES", widths["unres"])
296
+ + header("TITLE", widths["title"])
297
+ + header("AUTHOR", widths["author"])
298
+ + header("UPDATED", widths["age"])
299
+ )
300
+
301
+ links_enabled = ctx.obj.get("links_enabled", False)
302
+ console_url = get_console_url(config.public_api_url) if links_enabled else ""
303
+ slug = get_org_slug(client, org_id) if links_enabled else ""
304
+
305
+ for pull in pulls:
306
+ repository = pull.get("repository_full_name") or pull.get("repository_id") or "?"
307
+ number = pull.get("number")
308
+ pr_label = f"#{number}" if number is not None else "?"
309
+ state = _display_state(pull)
310
+ checks = pull.get("check_status") or "-"
311
+ merge = _merge_status(pull)
312
+ unresolved = pull.get("unresolved_thread_count")
313
+ unresolved = "?" if unresolved is None else unresolved
314
+ title = pull.get("title") or ""
315
+ author = pull.get("author_login") or "?"
316
+ age = format_relative_timestamp(pull.get("updated_at"))
317
+
318
+ repository_cell = cell(repository, widths["repo"])
319
+ pr_cell = click.style(cell(pr_label, widths["pr"]), fg="cyan")
320
+ title_cell = cell(title, widths["title"])
321
+ repo_id = pull.get("repository_id")
322
+ if links_enabled and repo_id and isinstance(number, int):
323
+ url = pr_url(console_url, slug, str(repo_id), number)
324
+ pr_cell = hyperlink(pr_cell, url)
325
+ title_cell = hyperlink(title_cell, url)
326
+
327
+ click.echo(
328
+ " "
329
+ + pr_cell
330
+ + (repository_cell if show_repository else "")
331
+ + cell(state, widths["state"])
332
+ + cell(checks, widths["checks"])
333
+ + cell(merge, widths["merge"])
334
+ + cell(unresolved, widths["unres"])
335
+ + title_cell
336
+ + cell(author, widths["author"])
337
+ + click.style(cell(age, widths["age"]), dim=True)
338
+ )