python-gitea 0.12.2__py3-none-any.whl → 0.13.1__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.
- gitea/cli/issue/get.py +164 -21
- gitea/cli/issue/ids.py +85 -0
- gitea/cli/issue/list.py +117 -23
- gitea/cli/project/column/issues.py +27 -3
- gitea/cli/project/column/list.py +11 -1
- gitea/cli/project/issues.py +9 -1
- gitea/cli/utils/projection.py +61 -0
- gitea/issue/project_column.py +123 -19
- {python_gitea-0.12.2.dist-info → python_gitea-0.13.1.dist-info}/METADATA +1 -1
- {python_gitea-0.12.2.dist-info → python_gitea-0.13.1.dist-info}/RECORD +13 -11
- {python_gitea-0.12.2.dist-info → python_gitea-0.13.1.dist-info}/WHEEL +0 -0
- {python_gitea-0.12.2.dist-info → python_gitea-0.13.1.dist-info}/entry_points.txt +0 -0
- {python_gitea-0.12.2.dist-info → python_gitea-0.13.1.dist-info}/licenses/LICENSE +0 -0
gitea/cli/issue/get.py
CHANGED
|
@@ -7,22 +7,54 @@ An earlier version of this command renamed it to `comment_count` to say so, and
|
|
|
7
7
|
was the only command emitting an issue that did - the same count arrived under
|
|
8
8
|
two names depending on which command fetched the issue, which was the worse
|
|
9
9
|
surprise of the two.
|
|
10
|
+
|
|
11
|
+
Several issues can be read in one run: `--issue-id` may be repeated, and
|
|
12
|
+
`--issue-id-file` reads a list of numbers from a file or from stdin, so a set of
|
|
13
|
+
hundreds need not fit on a command line. The shape of `data` follows from how
|
|
14
|
+
the command was invoked, never from what the API answered: one `--issue-id` and
|
|
15
|
+
no file gives the issue itself, as it always has; anything else gives a list of
|
|
16
|
+
issues in the order they were asked for, each once. An issue that does not exist
|
|
17
|
+
fails the run, after every requested issue has been tried, with an error naming
|
|
18
|
+
each missing one, so a single typo in a long list is not reported one run at a
|
|
19
|
+
time.
|
|
20
|
+
|
|
21
|
+
Every project an issue is on is emitted with the column its card sits in there,
|
|
22
|
+
resolved from that project's board: the issue payload names the projects without
|
|
23
|
+
saying where on them the card sits, so the column costs a walk of the board's
|
|
24
|
+
columns and their issue listings, a few requests per project. `--no-columns`
|
|
25
|
+
turns that walk off. Every project entry is then emitted as the API sent it,
|
|
26
|
+
without a `column_id`, and the run reads no board at all - one request per issue,
|
|
27
|
+
whatever boards the issues are on. It is the form to use when the issue's own
|
|
28
|
+
fields are what is wanted - `state`, `labels`, `title` - since it is the board
|
|
29
|
+
walk that a read of a set of issues pays most for.
|
|
10
30
|
"""
|
|
11
31
|
|
|
12
32
|
from __future__ import annotations
|
|
13
33
|
|
|
34
|
+
from collections.abc import Callable
|
|
14
35
|
from typing import Annotated, Any
|
|
15
36
|
|
|
16
37
|
import typer
|
|
17
38
|
|
|
18
|
-
from gitea.cli.
|
|
39
|
+
from gitea.cli.issue.ids import ISSUE_ID_FILE_HELP
|
|
40
|
+
from gitea.cli.utils.options import DEPRECATED_INDEX_HELP, REPOSITORY_REQUIRED_HELP
|
|
41
|
+
|
|
42
|
+
_NOT_FOUND = 404
|
|
43
|
+
|
|
44
|
+
ISSUE_IDS_HELP = "Issue number shown in the web UI. Repeat the option to read several issues."
|
|
45
|
+
ISSUE_COLUMNS_HELP = (
|
|
46
|
+
"Resolve the project column of each issue's card. Pass --no-columns to leave every project entry "
|
|
47
|
+
"as the API sent it and read no board, which is what a read of the issue's own fields wants."
|
|
48
|
+
)
|
|
19
49
|
|
|
20
50
|
|
|
21
51
|
def get_command(
|
|
22
52
|
ctx: typer.Context,
|
|
23
53
|
owner: Annotated[str, typer.Option("--owner", help="Owner of the repository.")],
|
|
24
54
|
repository: Annotated[str | None, typer.Option("--repository", help=REPOSITORY_REQUIRED_HELP)] = None,
|
|
25
|
-
|
|
55
|
+
issue_ids: Annotated[list[int] | None, typer.Option("--issue-id", help=ISSUE_IDS_HELP)] = None,
|
|
56
|
+
issue_id_file: Annotated[str | None, typer.Option("--issue-id-file", help=ISSUE_ID_FILE_HELP)] = None,
|
|
57
|
+
resolve_columns: Annotated[bool, typer.Option("--columns/--no-columns", help=ISSUE_COLUMNS_HELP)] = True,
|
|
26
58
|
index: Annotated[int | None, typer.Option("--index", help=DEPRECATED_INDEX_HELP, hidden=True)] = None,
|
|
27
59
|
account_name: Annotated[
|
|
28
60
|
str | None,
|
|
@@ -46,14 +78,16 @@ def get_command(
|
|
|
46
78
|
),
|
|
47
79
|
] = None,
|
|
48
80
|
) -> None:
|
|
49
|
-
"""Get
|
|
81
|
+
"""Get one or more issues in a repository.
|
|
50
82
|
|
|
51
83
|
Args:
|
|
52
84
|
ctx: The Typer context.
|
|
53
85
|
owner: The owner of the repository.
|
|
54
86
|
repository: The name of the repository, which this command requires.
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
issue_ids: The issue numbers shown in the web UI.
|
|
88
|
+
issue_id_file: A file listing more issue numbers, or `-` to read them from stdin.
|
|
89
|
+
resolve_columns: Whether to resolve the column each issue's card sits in.
|
|
90
|
+
index: The deprecated name of `--issue-id`, which names one issue.
|
|
57
91
|
account_name: Name of the account to use for authentication.
|
|
58
92
|
token: Token for authentication. If not provided, the token from the specified account will be used.
|
|
59
93
|
base_url: Base URL of the Gitea platform. If not provided, the base URL from the specified account will be used.
|
|
@@ -61,10 +95,11 @@ def get_command(
|
|
|
61
95
|
"""
|
|
62
96
|
from gitea.cli.utils.api import execute_api_command # noqa: PLC0415
|
|
63
97
|
from gitea.cli.utils.auth import get_auth_params # noqa: PLC0415
|
|
64
|
-
from gitea.cli.utils.options import require_repository
|
|
98
|
+
from gitea.cli.utils.options import require_repository # noqa: PLC0415
|
|
65
99
|
from gitea.client.gitea import Gitea # noqa: PLC0415
|
|
66
|
-
from gitea.issue.project_column import resolve_project_column_ids # noqa: PLC0415
|
|
100
|
+
from gitea.issue.project_column import ColumnListings, resolve_project_column_ids # noqa: PLC0415
|
|
67
101
|
|
|
102
|
+
command = "gitea-cli issue get"
|
|
68
103
|
token, base_url = get_auth_params(
|
|
69
104
|
config_path=ctx.obj.get("config_path"),
|
|
70
105
|
account_name=account_name,
|
|
@@ -72,27 +107,135 @@ def get_command(
|
|
|
72
107
|
base_url=base_url,
|
|
73
108
|
)
|
|
74
109
|
|
|
75
|
-
def api_call() -> tuple[dict[str, Any], dict[str, Any]]:
|
|
76
|
-
"""Get
|
|
110
|
+
def api_call() -> tuple[dict[str, Any] | list[dict[str, Any]], dict[str, Any]]:
|
|
111
|
+
"""Get the issues' information.
|
|
77
112
|
|
|
78
|
-
The column of each project
|
|
113
|
+
The column of each project an issue is on is resolved from the project's
|
|
79
114
|
board, because the issue payload names the projects without saying where
|
|
80
|
-
on them the issue's card sits.
|
|
115
|
+
on them the issue's card sits. The issues of one run share the listings
|
|
116
|
+
of their boards' columns, so a board is listed once however many of the
|
|
117
|
+
issues are on it. `--no-columns` asks for no such resolution: the issues
|
|
118
|
+
are emitted as the API sent them and no board is read, which is what a
|
|
119
|
+
read of the issues' own fields wants.
|
|
81
120
|
|
|
82
121
|
Returns:
|
|
83
|
-
A tuple containing the issue data and metadata.
|
|
122
|
+
A tuple containing the issue data, or a list of it, and metadata.
|
|
84
123
|
|
|
85
124
|
"""
|
|
86
|
-
target_repository = require_repository(repository, command=
|
|
87
|
-
|
|
125
|
+
target_repository = require_repository(repository, command=command)
|
|
126
|
+
targets, single = _requested_issues(issue_ids, issue_id_file, index, command=command)
|
|
88
127
|
|
|
89
128
|
with Gitea(token=token, base_url=base_url) as client:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
index=
|
|
129
|
+
columns = ColumnListings()
|
|
130
|
+
|
|
131
|
+
def get_one(number: int) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
132
|
+
data, metadata = client.issue.get_issue(owner=owner, repository=target_repository, index=number)
|
|
133
|
+
if not resolve_columns:
|
|
134
|
+
return data, metadata
|
|
135
|
+
data = resolve_project_column_ids(
|
|
136
|
+
client=client, owner=owner, repository=target_repository, issue=data, columns=columns
|
|
137
|
+
)
|
|
138
|
+
return data, metadata
|
|
139
|
+
|
|
140
|
+
if single:
|
|
141
|
+
return get_one(targets[0])
|
|
142
|
+
return _get_each(targets, get_one, owner=owner, repository=target_repository, command=command)
|
|
143
|
+
|
|
144
|
+
execute_api_command(api_call=api_call, base_url=base_url, command_name=command)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _requested_issues(
|
|
148
|
+
issue_ids: list[int] | None, issue_id_file: str | None, index: int | None, *, command: str
|
|
149
|
+
) -> tuple[list[int], bool]:
|
|
150
|
+
"""Read which issues the command reads, and whether it was asked for exactly one.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
issue_ids: The values passed as --issue-id, or None when it was omitted.
|
|
154
|
+
issue_id_file: The value passed as --issue-id-file, or None when it was omitted.
|
|
155
|
+
index: The value passed as the deprecated --index, or None when it was omitted.
|
|
156
|
+
command: The command being run, named as the user invoked it.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The issue numbers, in the order given with those of the file last, each
|
|
160
|
+
once; and True when one --issue-id (or --index) and no file named them,
|
|
161
|
+
which is the invocation that reports the issue itself rather than a list.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
CommandError: If no issue was given, or --index was combined with
|
|
165
|
+
several issues.
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
from gitea.cli.issue.ids import read_issue_id_file # noqa: PLC0415
|
|
169
|
+
from gitea.cli.utils.errors import CommandError # noqa: PLC0415
|
|
170
|
+
from gitea.cli.utils.options import resolve_issue_id # noqa: PLC0415
|
|
171
|
+
|
|
172
|
+
given = list(issue_ids or [])
|
|
173
|
+
if index is not None:
|
|
174
|
+
if len(given) > 1 or issue_id_file is not None:
|
|
175
|
+
raise CommandError(
|
|
176
|
+
f"'{command}': --index names one issue and cannot be combined with several. "
|
|
177
|
+
f"It is the deprecated name of --issue-id: pass --issue-id once per issue instead."
|
|
178
|
+
)
|
|
179
|
+
given = [resolve_issue_id(issue_id=given[0] if given else None, index=index, command=command)]
|
|
180
|
+
|
|
181
|
+
if issue_id_file is None:
|
|
182
|
+
if not given:
|
|
183
|
+
raise CommandError(
|
|
184
|
+
f"'{command}' needs an issue: pass --issue-id NUMBER, once per issue, or --issue-id-file PATH."
|
|
94
185
|
)
|
|
95
|
-
|
|
96
|
-
|
|
186
|
+
return list(dict.fromkeys(given)), len(given) == 1
|
|
187
|
+
|
|
188
|
+
given += read_issue_id_file(issue_id_file, command=command)
|
|
189
|
+
return list(dict.fromkeys(given)), False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _get_each(
|
|
193
|
+
numbers: list[int],
|
|
194
|
+
get_one: Callable[[int], tuple[dict[str, Any], dict[str, Any]]],
|
|
195
|
+
*,
|
|
196
|
+
owner: str,
|
|
197
|
+
repository: str,
|
|
198
|
+
command: str,
|
|
199
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
200
|
+
"""Read each issue in turn, and report every one that does not exist together.
|
|
97
201
|
|
|
98
|
-
|
|
202
|
+
Args:
|
|
203
|
+
numbers: The issues to read.
|
|
204
|
+
get_one: Callable reading one issue and returning its data and metadata.
|
|
205
|
+
owner: The owner of the repository.
|
|
206
|
+
repository: The name of the repository.
|
|
207
|
+
command: The command being run, named as the user invoked it.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
A tuple containing the issues, in the order asked for, and the metadata
|
|
211
|
+
of the last response.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
CommandError: If any of the issues does not exist. Any other failure is
|
|
215
|
+
raised as it was, on the issue that met it.
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
from requests import HTTPError # noqa: PLC0415
|
|
219
|
+
|
|
220
|
+
from gitea.cli.utils.errors import CommandError # noqa: PLC0415
|
|
221
|
+
|
|
222
|
+
issues: list[dict[str, Any]] = []
|
|
223
|
+
missing: list[int] = []
|
|
224
|
+
metadata: dict[str, Any] = {}
|
|
225
|
+
for number in numbers:
|
|
226
|
+
try:
|
|
227
|
+
data, metadata = get_one(number)
|
|
228
|
+
except HTTPError as e:
|
|
229
|
+
response = getattr(e, "response", None)
|
|
230
|
+
if response is None or response.status_code != _NOT_FOUND:
|
|
231
|
+
raise
|
|
232
|
+
missing.append(number)
|
|
233
|
+
continue
|
|
234
|
+
issues.append(data)
|
|
235
|
+
|
|
236
|
+
if missing:
|
|
237
|
+
raise CommandError(
|
|
238
|
+
f"'{command}': no issue {', '.join(str(number) for number in missing)} in {owner}/{repository}; "
|
|
239
|
+
f"the other {len(issues)} of the {len(numbers)} requested exist. Nothing was printed."
|
|
240
|
+
)
|
|
241
|
+
return issues, {"status_code": metadata["status_code"]}
|
gitea/cli/issue/ids.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Reading a set of issue numbers given as text: a comma list, a file, or stdin.
|
|
2
|
+
|
|
3
|
+
`issue get` reads several issues at once and `issue list` can be narrowed to a
|
|
4
|
+
known set, so both need the numbers as a set rather than one option value per
|
|
5
|
+
issue. The numbers are separated by commas, whitespace or newlines, in any mix,
|
|
6
|
+
so a list written by hand, one printed by `jq -r '.[]'`, and one pasted from a
|
|
7
|
+
comma-separated cell all read the same. A value that is not a positive whole
|
|
8
|
+
number is an error naming it, rather than a number quietly dropped.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from gitea.cli.utils.errors import CommandError
|
|
18
|
+
|
|
19
|
+
# The value of `--issue-id-file` that asks for the numbers to be read from stdin.
|
|
20
|
+
FROM_STDIN = "-"
|
|
21
|
+
|
|
22
|
+
ISSUE_ID_FILE_HELP = (
|
|
23
|
+
"File listing issue numbers, separated by commas, whitespace or newlines. Pass '-' to read them from stdin."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_SEPARATORS = re.compile(r"[\s,]+")
|
|
27
|
+
_ISSUE_NUMBER = re.compile(r"[0-9]+")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_issue_numbers(text: str, *, source: str, command: str) -> list[int]:
|
|
31
|
+
"""Read the issue numbers in a piece of text.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
text: The numbers, separated by commas, whitespace or newlines.
|
|
35
|
+
source: Where the text came from, for the error, e.g. "--issue-ids".
|
|
36
|
+
command: The command being run, named as the user invoked it.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The numbers, in the order given, repeats included.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
CommandError: If a value is not a positive whole number.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
tokens = [token for token in _SEPARATORS.split(text) if token]
|
|
46
|
+
invalid = [token for token in tokens if not _ISSUE_NUMBER.fullmatch(token) or int(token) == 0]
|
|
47
|
+
if invalid:
|
|
48
|
+
raise CommandError(
|
|
49
|
+
f"'{command}': {source} holds {', '.join(repr(token) for token in invalid)}, "
|
|
50
|
+
f"which {'is not an issue number' if len(invalid) == 1 else 'are not issue numbers'}. "
|
|
51
|
+
f"Issue numbers are the positive whole numbers shown in the web UI."
|
|
52
|
+
)
|
|
53
|
+
return [int(token) for token in tokens]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def read_issue_id_file(path: str, *, command: str) -> list[int]:
|
|
57
|
+
"""Read the issue numbers listed in a file, or on stdin.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: The value passed as --issue-id-file, or `-` to read stdin.
|
|
61
|
+
command: The command being run, named as the user invoked it.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The numbers, in the order listed, repeats included.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
CommandError: If the file cannot be read, lists no number, or lists a
|
|
68
|
+
value that is not one. A file listing nothing is an error rather than
|
|
69
|
+
an empty result, since that is what a pipeline that failed upstream
|
|
70
|
+
looks like.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
source = "stdin" if path == FROM_STDIN else path
|
|
74
|
+
if path == FROM_STDIN:
|
|
75
|
+
text = sys.stdin.read()
|
|
76
|
+
else:
|
|
77
|
+
try:
|
|
78
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
79
|
+
except OSError as e:
|
|
80
|
+
raise CommandError(f"'{command}' could not read --issue-id-file {path}: {e.strerror or e}.") from e
|
|
81
|
+
|
|
82
|
+
numbers = parse_issue_numbers(text, source=f"--issue-id-file {source}", command=command)
|
|
83
|
+
if not numbers:
|
|
84
|
+
raise CommandError(f"'{command}': --issue-id-file {source} lists no issue number.")
|
|
85
|
+
return numbers
|
gitea/cli/issue/list.py
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
"""List issues command.
|
|
1
|
+
"""List issues command.
|
|
2
|
+
|
|
3
|
+
`--issue-ids` narrows the listing to a known set of issues. Gitea's listing takes
|
|
4
|
+
no such filter, so the set is applied here, over the listing the other filters
|
|
5
|
+
select: its pages are walked until every requested issue has been seen or the
|
|
6
|
+
listing ends, and the issues are emitted in the order they were asked for. An
|
|
7
|
+
issue of the set that the other filters exclude - a closed one under the default
|
|
8
|
+
`--state open`, say - is simply not listed, which is what makes "which of these
|
|
9
|
+
are open" one call; `metadata.not_listed` names each such issue, so it can be
|
|
10
|
+
told apart from one that was listed. `issue get` is the command that fails on an
|
|
11
|
+
issue that does not exist.
|
|
12
|
+
"""
|
|
2
13
|
|
|
3
14
|
from __future__ import annotations
|
|
4
15
|
|
|
16
|
+
from collections.abc import Callable
|
|
5
17
|
from datetime import datetime
|
|
6
|
-
from typing import Annotated, Literal
|
|
18
|
+
from typing import Annotated, Any, Literal
|
|
7
19
|
|
|
8
20
|
import typer
|
|
9
21
|
|
|
10
22
|
from gitea.cli.utils.options import REPOSITORY_REQUIRED_HELP
|
|
11
23
|
|
|
24
|
+
ISSUE_IDS_FILTER_HELP = (
|
|
25
|
+
"List only these issues, given as numbers separated by commas, in the order given. "
|
|
26
|
+
"The other filters still apply. Cannot be combined with --page."
|
|
27
|
+
)
|
|
28
|
+
|
|
12
29
|
|
|
13
30
|
def list_command(
|
|
14
31
|
ctx: typer.Context,
|
|
@@ -53,6 +70,10 @@ def list_command(
|
|
|
53
70
|
str | None,
|
|
54
71
|
typer.Option("--mentioned-by", help="Filter issues mentioning this user."),
|
|
55
72
|
] = None,
|
|
73
|
+
issue_ids: Annotated[
|
|
74
|
+
str | None,
|
|
75
|
+
typer.Option("--issue-ids", help=ISSUE_IDS_FILTER_HELP),
|
|
76
|
+
] = None,
|
|
56
77
|
page: Annotated[
|
|
57
78
|
int | None,
|
|
58
79
|
typer.Option("--page", help="The page number for pagination."),
|
|
@@ -99,6 +120,7 @@ def list_command(
|
|
|
99
120
|
created_by: Filter issues created by this user.
|
|
100
121
|
assigned_by: Filter issues assigned to this user.
|
|
101
122
|
mentioned_by: Filter issues mentioning this user.
|
|
123
|
+
issue_ids: List only these issues, given as comma-separated numbers.
|
|
102
124
|
page: The page number for pagination.
|
|
103
125
|
limit: The number of issues per page.
|
|
104
126
|
account_name: Name of the account to use for authentication.
|
|
@@ -106,14 +128,14 @@ def list_command(
|
|
|
106
128
|
base_url: Base URL of the Gitea platform. If not provided, the base URL from the specified account will be used.
|
|
107
129
|
|
|
108
130
|
"""
|
|
109
|
-
from typing import Any # noqa: PLC0415
|
|
110
|
-
|
|
111
131
|
from gitea.cli.utils.api import execute_api_command # noqa: PLC0415
|
|
112
132
|
from gitea.cli.utils.auth import get_auth_params # noqa: PLC0415
|
|
113
133
|
from gitea.cli.utils.convert import list_str_to_list_int_or_none # noqa: PLC0415
|
|
114
134
|
from gitea.cli.utils.options import require_repository # noqa: PLC0415
|
|
115
135
|
from gitea.client.gitea import Gitea # noqa: PLC0415
|
|
136
|
+
from gitea.utils.pagination import PAGE_SIZE # noqa: PLC0415
|
|
116
137
|
|
|
138
|
+
command = "gitea-cli issue list"
|
|
117
139
|
token, base_url = get_auth_params(
|
|
118
140
|
config_path=ctx.obj.get("config_path"),
|
|
119
141
|
account_name=account_name,
|
|
@@ -130,24 +152,96 @@ def list_command(
|
|
|
130
152
|
The issue information as a dictionary.
|
|
131
153
|
|
|
132
154
|
"""
|
|
133
|
-
target_repository = require_repository(repository, command=
|
|
155
|
+
target_repository = require_repository(repository, command=command)
|
|
156
|
+
requested = _requested_issues(issue_ids, page, command=command)
|
|
134
157
|
|
|
135
158
|
with Gitea(token=token, base_url=base_url) as client:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
|
|
160
|
+
def fetch(page_number: int | None, page_size: int | None) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
161
|
+
return client.issue.list_issues(
|
|
162
|
+
owner=owner,
|
|
163
|
+
repository=target_repository,
|
|
164
|
+
state=state,
|
|
165
|
+
labels=labels,
|
|
166
|
+
search_string=search_string,
|
|
167
|
+
issue_type=issue_type,
|
|
168
|
+
milestones=milestones_values,
|
|
169
|
+
since=since,
|
|
170
|
+
before=before,
|
|
171
|
+
created_by=created_by,
|
|
172
|
+
assigned_by=assigned_by,
|
|
173
|
+
mentioned_by=mentioned_by,
|
|
174
|
+
page=page_number,
|
|
175
|
+
limit=page_size,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if requested is None:
|
|
179
|
+
return fetch(page, limit)
|
|
180
|
+
return _list_requested(requested, lambda number: fetch(number, limit or PAGE_SIZE))
|
|
181
|
+
|
|
182
|
+
execute_api_command(api_call=api_call, base_url=base_url, command_name=command)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _requested_issues(issue_ids: str | None, page: int | None, *, command: str) -> list[int] | None:
|
|
186
|
+
"""Read the set of issues the listing is narrowed to.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
issue_ids: The value passed as --issue-ids, or None when it was omitted.
|
|
190
|
+
page: The value passed as --page, or None when it was omitted.
|
|
191
|
+
command: The command being run, named as the user invoked it.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The issue numbers in the order given, each once, or None when the
|
|
195
|
+
listing is not narrowed.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
CommandError: If the set is empty or holds a value that is not an issue
|
|
199
|
+
number, or --page was passed with it: the set is looked for across
|
|
200
|
+
every page, so a page number would have nothing to select.
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
from gitea.cli.issue.ids import parse_issue_numbers # noqa: PLC0415
|
|
204
|
+
from gitea.cli.utils.errors import CommandError # noqa: PLC0415
|
|
205
|
+
|
|
206
|
+
if issue_ids is None:
|
|
207
|
+
return None
|
|
208
|
+
if page is not None:
|
|
209
|
+
raise CommandError(
|
|
210
|
+
f"'{command}': --issue-ids is looked for across every page of the listing, so --page cannot be "
|
|
211
|
+
f"combined with it. Drop --page; --limit still sets how many issues each request asks for."
|
|
212
|
+
)
|
|
213
|
+
numbers = parse_issue_numbers(issue_ids, source="--issue-ids", command=command)
|
|
214
|
+
if not numbers:
|
|
215
|
+
raise CommandError(f"'{command}': --issue-ids names no issue. Pass the numbers separated by commas.")
|
|
216
|
+
return list(dict.fromkeys(numbers))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _list_requested(
|
|
220
|
+
numbers: list[int],
|
|
221
|
+
fetch_page: Callable[[int], tuple[list[dict[str, Any]], dict[str, Any]]],
|
|
222
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
223
|
+
"""Walk a listing until every requested issue has been seen, and keep only those.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
numbers: The requested issue numbers, each once.
|
|
227
|
+
fetch_page: Callable returning the items and metadata of the given page number.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
A tuple containing the requested issues the listing holds, in the order
|
|
231
|
+
requested, and the metadata of the last response with `not_listed`
|
|
232
|
+
naming the requested issues it does not hold.
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
from gitea.utils.pagination import iter_pages # noqa: PLC0415
|
|
236
|
+
|
|
237
|
+
wanted = set(numbers)
|
|
238
|
+
found: dict[int, dict[str, Any]] = {}
|
|
239
|
+
metadata: dict[str, Any] = {}
|
|
240
|
+
for batch, page_metadata in iter_pages(fetch_page):
|
|
241
|
+
metadata = page_metadata
|
|
242
|
+
found.update({issue["number"]: issue for issue in batch if issue.get("number") in wanted})
|
|
243
|
+
if len(found) == len(wanted):
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
issues = [found[number] for number in numbers if number in found]
|
|
247
|
+
return issues, {**metadata, "not_listed": [number for number in numbers if number not in found]}
|
|
@@ -6,6 +6,8 @@ from typing import Annotated
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
+
from gitea.cli.utils.projection import FIELDS_HELP
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def list_column_issues_command(
|
|
11
13
|
ctx: typer.Context,
|
|
@@ -24,6 +26,10 @@ def list_column_issues_command(
|
|
|
24
26
|
str | None,
|
|
25
27
|
typer.Option("--repository", help="Name of the repository. Omit for organization projects."),
|
|
26
28
|
] = None,
|
|
29
|
+
fields: Annotated[
|
|
30
|
+
str | None,
|
|
31
|
+
typer.Option("--fields", help=FIELDS_HELP),
|
|
32
|
+
] = None,
|
|
27
33
|
account_name: Annotated[
|
|
28
34
|
str | None,
|
|
29
35
|
typer.Option(
|
|
@@ -46,7 +52,11 @@ def list_column_issues_command(
|
|
|
46
52
|
),
|
|
47
53
|
] = None,
|
|
48
54
|
) -> None:
|
|
49
|
-
"""List the issues in a project's column.
|
|
55
|
+
"""List the issues in a project's column, together with the column read.
|
|
56
|
+
|
|
57
|
+
The column is echoed by the ID and title the API reports for it, so a read of
|
|
58
|
+
the wrong column of the right project says which column it read instead of
|
|
59
|
+
passing for the intended one.
|
|
50
60
|
|
|
51
61
|
Args:
|
|
52
62
|
ctx: The Typer context.
|
|
@@ -56,6 +66,7 @@ def list_column_issues_command(
|
|
|
56
66
|
column_id: The ID of the column.
|
|
57
67
|
page: The page number for pagination.
|
|
58
68
|
limit: The number of issues per page.
|
|
69
|
+
fields: Comma-separated fields to keep on each issue, or None for all of them.
|
|
59
70
|
account_name: Name of the account to use for authentication.
|
|
60
71
|
token: Token for authentication.
|
|
61
72
|
base_url: Base URL of the Gitea platform.
|
|
@@ -65,6 +76,7 @@ def list_column_issues_command(
|
|
|
65
76
|
|
|
66
77
|
from gitea.cli.utils.api import execute_api_command # noqa: PLC0415
|
|
67
78
|
from gitea.cli.utils.auth import get_auth_params # noqa: PLC0415
|
|
79
|
+
from gitea.cli.utils.projection import parse_fields, project_records # noqa: PLC0415
|
|
68
80
|
from gitea.client.gitea import Gitea # noqa: PLC0415
|
|
69
81
|
|
|
70
82
|
token, base_url = get_auth_params(
|
|
@@ -78,11 +90,18 @@ def list_column_issues_command(
|
|
|
78
90
|
"""List the issues in a project column.
|
|
79
91
|
|
|
80
92
|
Returns:
|
|
81
|
-
A tuple containing the
|
|
93
|
+
A tuple containing the column read with its issues, and metadata.
|
|
82
94
|
|
|
83
95
|
"""
|
|
96
|
+
names = parse_fields(fields)
|
|
84
97
|
with Gitea(token=token, base_url=base_url) as client:
|
|
85
|
-
|
|
98
|
+
column, _ = client.project.get_project_column(
|
|
99
|
+
owner=owner,
|
|
100
|
+
repository=repository,
|
|
101
|
+
project_id=project_id,
|
|
102
|
+
column_id=column_id,
|
|
103
|
+
)
|
|
104
|
+
issues, metadata = client.project.list_project_column_issues(
|
|
86
105
|
owner=owner,
|
|
87
106
|
repository=repository,
|
|
88
107
|
project_id=project_id,
|
|
@@ -90,5 +109,10 @@ def list_column_issues_command(
|
|
|
90
109
|
page=page,
|
|
91
110
|
limit=limit,
|
|
92
111
|
)
|
|
112
|
+
data = {
|
|
113
|
+
"column": {"id": column.get("id"), "title": column.get("title")},
|
|
114
|
+
"issues": project_records(issues, names),
|
|
115
|
+
}
|
|
116
|
+
return data, metadata
|
|
93
117
|
|
|
94
118
|
execute_api_command(api_call=api_call, base_url=base_url, command_name="gitea-cli project column issues")
|
gitea/cli/project/column/list.py
CHANGED
|
@@ -6,6 +6,8 @@ from typing import Annotated
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
+
from gitea.cli.utils.projection import FIELDS_HELP
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def list_columns_command(
|
|
11
13
|
ctx: typer.Context,
|
|
@@ -23,6 +25,10 @@ def list_columns_command(
|
|
|
23
25
|
str | None,
|
|
24
26
|
typer.Option("--repository", help="Name of the repository. Omit for organization projects."),
|
|
25
27
|
] = None,
|
|
28
|
+
fields: Annotated[
|
|
29
|
+
str | None,
|
|
30
|
+
typer.Option("--fields", help=FIELDS_HELP),
|
|
31
|
+
] = None,
|
|
26
32
|
account_name: Annotated[
|
|
27
33
|
str | None,
|
|
28
34
|
typer.Option(
|
|
@@ -54,6 +60,7 @@ def list_columns_command(
|
|
|
54
60
|
project_id: The ID of the project.
|
|
55
61
|
page: The page number for pagination.
|
|
56
62
|
limit: The number of columns per page.
|
|
63
|
+
fields: Comma-separated fields to keep on each column, or None for all of them.
|
|
57
64
|
account_name: Name of the account to use for authentication.
|
|
58
65
|
token: Token for authentication.
|
|
59
66
|
base_url: Base URL of the Gitea platform.
|
|
@@ -63,6 +70,7 @@ def list_columns_command(
|
|
|
63
70
|
|
|
64
71
|
from gitea.cli.utils.api import execute_api_command # noqa: PLC0415
|
|
65
72
|
from gitea.cli.utils.auth import get_auth_params # noqa: PLC0415
|
|
73
|
+
from gitea.cli.utils.projection import parse_fields, project_records # noqa: PLC0415
|
|
66
74
|
from gitea.client.gitea import Gitea # noqa: PLC0415
|
|
67
75
|
|
|
68
76
|
token, base_url = get_auth_params(
|
|
@@ -79,13 +87,15 @@ def list_columns_command(
|
|
|
79
87
|
A tuple containing the column data and metadata.
|
|
80
88
|
|
|
81
89
|
"""
|
|
90
|
+
names = parse_fields(fields)
|
|
82
91
|
with Gitea(token=token, base_url=base_url) as client:
|
|
83
|
-
|
|
92
|
+
columns, metadata = client.project.list_project_columns(
|
|
84
93
|
owner=owner,
|
|
85
94
|
repository=repository,
|
|
86
95
|
project_id=project_id,
|
|
87
96
|
page=page,
|
|
88
97
|
limit=limit,
|
|
89
98
|
)
|
|
99
|
+
return project_records(columns, names), metadata
|
|
90
100
|
|
|
91
101
|
execute_api_command(api_call=api_call, base_url=base_url, command_name="gitea-cli project column list")
|
gitea/cli/project/issues.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Annotated, Any
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
+
from gitea.cli.utils.projection import FIELDS_HELP
|
|
9
10
|
from gitea.utils.pagination import PAGE_SIZE, collect_all_pages
|
|
10
11
|
|
|
11
12
|
|
|
@@ -17,6 +18,10 @@ def list_project_issues_command(
|
|
|
17
18
|
str | None,
|
|
18
19
|
typer.Option("--repository", help="Name of the repository. Omit for organization projects."),
|
|
19
20
|
] = None,
|
|
21
|
+
fields: Annotated[
|
|
22
|
+
str | None,
|
|
23
|
+
typer.Option("--fields", help=FIELDS_HELP),
|
|
24
|
+
] = None,
|
|
20
25
|
account_name: Annotated[
|
|
21
26
|
str | None,
|
|
22
27
|
typer.Option(
|
|
@@ -46,6 +51,7 @@ def list_project_issues_command(
|
|
|
46
51
|
owner: The owner of the repository.
|
|
47
52
|
repository: The name of the repository, or None for organization projects.
|
|
48
53
|
project_id: The ID of the project.
|
|
54
|
+
fields: Comma-separated fields to keep on each issue, or None for all of them.
|
|
49
55
|
account_name: Name of the account to use for authentication.
|
|
50
56
|
token: Token for authentication.
|
|
51
57
|
base_url: Base URL of the Gitea platform.
|
|
@@ -53,6 +59,7 @@ def list_project_issues_command(
|
|
|
53
59
|
"""
|
|
54
60
|
from gitea.cli.utils.api import execute_api_command # noqa: PLC0415
|
|
55
61
|
from gitea.cli.utils.auth import get_auth_params # noqa: PLC0415
|
|
62
|
+
from gitea.cli.utils.projection import parse_fields, project_records # noqa: PLC0415
|
|
56
63
|
from gitea.client.gitea import Gitea # noqa: PLC0415
|
|
57
64
|
|
|
58
65
|
token, base_url = get_auth_params(
|
|
@@ -71,6 +78,7 @@ def list_project_issues_command(
|
|
|
71
78
|
A tuple containing one entry per column, each with its issues, and metadata.
|
|
72
79
|
|
|
73
80
|
"""
|
|
81
|
+
names = parse_fields(fields)
|
|
74
82
|
with Gitea(token=token, base_url=base_url) as client:
|
|
75
83
|
columns, metadata = collect_all_pages(
|
|
76
84
|
lambda page: client.project.list_project_columns(
|
|
@@ -99,7 +107,7 @@ def list_project_issues_command(
|
|
|
99
107
|
data.append(
|
|
100
108
|
{
|
|
101
109
|
"column": {"id": column["id"], "title": column.get("title")},
|
|
102
|
-
"issues": issues,
|
|
110
|
+
"issues": project_records(issues, names),
|
|
103
111
|
}
|
|
104
112
|
)
|
|
105
113
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Cut the records a listing emits down to the fields the caller asked for.
|
|
2
|
+
|
|
3
|
+
A board listing carries every issue as the API sends it, a full `user` object
|
|
4
|
+
and all, so a read meant to answer "which cards are where" runs to a hundred
|
|
5
|
+
kilobytes on a board of a dozen cards. `--fields` keeps the keys named and
|
|
6
|
+
drops the rest.
|
|
7
|
+
|
|
8
|
+
A field named but absent from a record is emitted as `null` rather than left
|
|
9
|
+
out, so a misspelled field shows up in the output as a column of nulls instead
|
|
10
|
+
of as records that quietly lost it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from gitea.cli.utils.errors import CommandError
|
|
18
|
+
|
|
19
|
+
FIELDS_HELP = (
|
|
20
|
+
"Comma-separated fields to keep on each record, e.g. id,number,title,state. "
|
|
21
|
+
"A field a record does not carry is emitted as null. Omit to emit every field."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_fields(fields: str | None) -> list[str] | None:
|
|
26
|
+
"""Read the value of `--fields`.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
fields: The value passed as --fields, or None when omitted.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The field names in the order given, without repeats, or None when the
|
|
33
|
+
option was omitted.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
CommandError: If the option was passed but names no field.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
if fields is None:
|
|
40
|
+
return None
|
|
41
|
+
names = list(dict.fromkeys(name.strip() for name in fields.split(",") if name.strip()))
|
|
42
|
+
if not names:
|
|
43
|
+
raise CommandError(f"--fields names no field: {fields!r}. Pass a comma-separated list, e.g. id,title.")
|
|
44
|
+
return names
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def project_records(records: list[dict[str, Any]], fields: list[str] | None) -> list[dict[str, Any]]:
|
|
48
|
+
"""Keep only the named fields of each record.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
records: The records of a listing.
|
|
52
|
+
fields: The fields to keep, or None to keep every field.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The records unchanged when `fields` is None, and otherwise each record
|
|
56
|
+
cut down to `fields`, in that order.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
if fields is None:
|
|
60
|
+
return records
|
|
61
|
+
return [{name: record.get(name) for name in fields} for record in records]
|
gitea/issue/project_column.py
CHANGED
|
@@ -34,6 +34,17 @@ lists the issue. It is the answer to "has this issue a card on this project, and
|
|
|
34
34
|
where" wherever that has to be told apart from "the board could not be read" -
|
|
35
35
|
the project issue commands ask it before moving a card, because Gitea's move
|
|
36
36
|
endpoint reports success without doing anything when there is no card to move.
|
|
37
|
+
|
|
38
|
+
A caller resolving many issues in one run - ``gitea-cli issue get`` given a list
|
|
39
|
+
of them - passes one ``ColumnListings`` to every lookup, so that a board the
|
|
40
|
+
issues share has its columns listed once for the run rather than once per issue.
|
|
41
|
+
The listing is still paged only as far as some walk needed it, and a lookup that
|
|
42
|
+
fails while paging it is forgotten rather than kept, so the next issue on that
|
|
43
|
+
board pages it afresh: each issue meets the failures a walk of its own would have
|
|
44
|
+
met, and only the requests already answered are not repeated. The columns' issue
|
|
45
|
+
listings are not kept, since they are what tells one issue's card from another's.
|
|
46
|
+
A column added to a board during the run is not seen by the issues after the
|
|
47
|
+
listing was paged, which is the same race the walk already has within one issue.
|
|
37
48
|
"""
|
|
38
49
|
|
|
39
50
|
from __future__ import annotations
|
|
@@ -47,6 +58,8 @@ from requests import RequestException
|
|
|
47
58
|
from gitea.utils.pagination import PAGE_SIZE, iter_async_pages, iter_pages
|
|
48
59
|
|
|
49
60
|
if TYPE_CHECKING:
|
|
61
|
+
from collections.abc import Iterator
|
|
62
|
+
|
|
50
63
|
from gitea.client.async_gitea import AsyncGitea
|
|
51
64
|
from gitea.client.gitea import Gitea
|
|
52
65
|
|
|
@@ -123,6 +136,101 @@ def _holds_issue(issues: list[Any], issue_id: int) -> bool:
|
|
|
123
136
|
return any(_identifier(issue) == issue_id for issue in issues)
|
|
124
137
|
|
|
125
138
|
|
|
139
|
+
class _ColumnListing:
|
|
140
|
+
"""The IDs of one project's columns, paged in as a walk asks for them.
|
|
141
|
+
|
|
142
|
+
Every walk over it replays the IDs already paged in and asks for the next
|
|
143
|
+
page only once it has passed them all, so walks sharing the listing request
|
|
144
|
+
each page once. A page that fails propagates its error and ends the listing.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, pages: Iterator[tuple[list[Any], dict[str, Any]]]) -> None:
|
|
148
|
+
"""Wrap the pages of a column listing.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
pages: The listing's pages, each fetched when it is first reached.
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
self._pages = pages
|
|
155
|
+
self._ids: list[int] = []
|
|
156
|
+
self._complete = False
|
|
157
|
+
|
|
158
|
+
def __iter__(self) -> Iterator[int]:
|
|
159
|
+
"""Walk the column IDs in board order.
|
|
160
|
+
|
|
161
|
+
Yields:
|
|
162
|
+
The ID of each column that carries a usable one.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
index = 0
|
|
166
|
+
while True:
|
|
167
|
+
if index < len(self._ids):
|
|
168
|
+
yield self._ids[index]
|
|
169
|
+
index += 1
|
|
170
|
+
continue
|
|
171
|
+
# An exhausted generator stops without a request, but `pages` may be any
|
|
172
|
+
# Iterator: once complete, never ask the source for another page.
|
|
173
|
+
if self._complete:
|
|
174
|
+
return
|
|
175
|
+
try:
|
|
176
|
+
columns, _ = next(self._pages)
|
|
177
|
+
except StopIteration:
|
|
178
|
+
self._complete = True
|
|
179
|
+
return
|
|
180
|
+
self._ids.extend(column_id for column in columns if (column_id := _identifier(column)) is not None)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ColumnListings:
|
|
184
|
+
"""The column listings of the projects one run has looked a card up on.
|
|
185
|
+
|
|
186
|
+
Meant to live for one run and no longer: a listing kept is not listed again,
|
|
187
|
+
so a board edited between two runs would otherwise be read as it was.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self) -> None:
|
|
191
|
+
"""Start with no project listed."""
|
|
192
|
+
self._listings: dict[tuple[str, str | None, int], _ColumnListing] = {}
|
|
193
|
+
|
|
194
|
+
def column_ids(self, *, client: Gitea, owner: str, repository: str | None, project_id: int) -> Iterator[int]:
|
|
195
|
+
"""Walk the IDs of a project's columns, listing them only as far as no walk has before.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
client: The Gitea client used for the lookups.
|
|
199
|
+
owner: The owner of the repository or organization holding the project.
|
|
200
|
+
repository: The name of the repository holding the project, or None for
|
|
201
|
+
an organization project.
|
|
202
|
+
project_id: The ID of the project.
|
|
203
|
+
|
|
204
|
+
Yields:
|
|
205
|
+
The ID of each column of the project, in board order.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
Exception: Whatever listing a page raised. The listing is then
|
|
209
|
+
forgotten, so the next walk of the project lists it afresh.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
key = (owner, repository, project_id)
|
|
213
|
+
listing = self._listings.get(key)
|
|
214
|
+
if listing is None:
|
|
215
|
+
listing = self._listings[key] = _ColumnListing(
|
|
216
|
+
iter_pages(
|
|
217
|
+
lambda page: client.project.list_project_columns(
|
|
218
|
+
owner=owner,
|
|
219
|
+
repository=repository,
|
|
220
|
+
project_id=project_id,
|
|
221
|
+
page=page,
|
|
222
|
+
limit=PAGE_SIZE,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
try:
|
|
227
|
+
yield from listing
|
|
228
|
+
except Exception:
|
|
229
|
+
if self._listings.get(key) is listing:
|
|
230
|
+
del self._listings[key]
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
|
|
126
234
|
_LOOKUP_FAILED = "Could not resolve the column of issue %s on project %s, reporting it as null: %s"
|
|
127
235
|
|
|
128
236
|
# aiohttp raises its total timeout as a bare asyncio.TimeoutError, which is the
|
|
@@ -138,6 +246,7 @@ def resolve_project_column_ids(
|
|
|
138
246
|
owner: str,
|
|
139
247
|
repository: str,
|
|
140
248
|
issue: dict[str, Any],
|
|
249
|
+
columns: ColumnListings | None = None,
|
|
141
250
|
) -> dict[str, Any]:
|
|
142
251
|
"""Populate the ``column_id`` of every project an issue is on.
|
|
143
252
|
|
|
@@ -146,6 +255,8 @@ def resolve_project_column_ids(
|
|
|
146
255
|
owner: The owner of the repository holding the issue.
|
|
147
256
|
repository: The name of the repository holding the issue.
|
|
148
257
|
issue: The issue data returned by the API.
|
|
258
|
+
columns: The column listings to share with the other issues of the run,
|
|
259
|
+
or None to list each project's columns for this issue alone.
|
|
149
260
|
|
|
150
261
|
Returns:
|
|
151
262
|
The issue data with a ``column_id`` on every project entry, holding the
|
|
@@ -178,6 +289,7 @@ def resolve_project_column_ids(
|
|
|
178
289
|
repository=_column_scope_repository(project, repository),
|
|
179
290
|
project_id=project_id,
|
|
180
291
|
issue_id=issue_id,
|
|
292
|
+
columns=columns,
|
|
181
293
|
)
|
|
182
294
|
except RequestException as e:
|
|
183
295
|
# Enriching the issue is not worth failing the issue over: the
|
|
@@ -195,6 +307,7 @@ def find_card_column_id(
|
|
|
195
307
|
repository: str | None,
|
|
196
308
|
project_id: int,
|
|
197
309
|
issue_id: int,
|
|
310
|
+
columns: ColumnListings | None = None,
|
|
198
311
|
) -> int | None:
|
|
199
312
|
"""Find the column of a project that lists an issue.
|
|
200
313
|
|
|
@@ -205,34 +318,25 @@ def find_card_column_id(
|
|
|
205
318
|
an organization project.
|
|
206
319
|
project_id: The ID of the project.
|
|
207
320
|
issue_id: The global ID of the issue.
|
|
321
|
+
columns: The column listings to share with the other lookups of the run,
|
|
322
|
+
or None to list the project's columns for this lookup alone.
|
|
208
323
|
|
|
209
324
|
Returns:
|
|
210
325
|
The ID of the column listing the issue, or None when no column of the
|
|
211
326
|
project lists it.
|
|
212
327
|
|
|
213
328
|
"""
|
|
214
|
-
|
|
215
|
-
|
|
329
|
+
listings = ColumnListings() if columns is None else columns
|
|
330
|
+
for column_id in listings.column_ids(client=client, owner=owner, repository=repository, project_id=project_id):
|
|
331
|
+
if column_holds_card(
|
|
332
|
+
client=client,
|
|
216
333
|
owner=owner,
|
|
217
334
|
repository=repository,
|
|
218
335
|
project_id=project_id,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
for column in columns:
|
|
224
|
-
column_id = _identifier(column)
|
|
225
|
-
if column_id is None:
|
|
226
|
-
continue
|
|
227
|
-
if column_holds_card(
|
|
228
|
-
client=client,
|
|
229
|
-
owner=owner,
|
|
230
|
-
repository=repository,
|
|
231
|
-
project_id=project_id,
|
|
232
|
-
column_id=column_id,
|
|
233
|
-
issue_id=issue_id,
|
|
234
|
-
):
|
|
235
|
-
return column_id
|
|
336
|
+
column_id=column_id,
|
|
337
|
+
issue_id=issue_id,
|
|
338
|
+
):
|
|
339
|
+
return column_id
|
|
236
340
|
return None
|
|
237
341
|
|
|
238
342
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-gitea
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: A Python package for interacting with the Gitea API, offering a simple interface to access repositories, users, organizations, issues, and more for automation and data management.
|
|
5
5
|
Project-URL: Documentation, https://isaac-cf-wong.github.io/python-gitea
|
|
6
6
|
Project-URL: Source, https://github.com/isaac-cf-wong/python-gitea
|
|
@@ -78,8 +78,9 @@ gitea/cli/issue/__init__.py,sha256=I-46LqvYmDgd08yMiFDPU2oE313uT-06nUUpVxs_dTg,4
|
|
|
78
78
|
gitea/cli/issue/close.py,sha256=3L1Hvwndq4aNl-G5KWueyJ-tnbHTP0NzXGfeACJZzE4,4127
|
|
79
79
|
gitea/cli/issue/create.py,sha256=g0x33xFt_USPJ0szspa7M_uWK728lnzDP6m0CYlJ0p4,4544
|
|
80
80
|
gitea/cli/issue/edit.py,sha256=QdUtWVBZoK2UjWXnAAvR-eMKmtOM2Yw51uy1sTsCKRo,5204
|
|
81
|
-
gitea/cli/issue/get.py,sha256=
|
|
82
|
-
gitea/cli/issue/
|
|
81
|
+
gitea/cli/issue/get.py,sha256=LYLZIwCVC67Ypn2WSoIo_6mz8eiseIlOjZEfLHRbfhI,10754
|
|
82
|
+
gitea/cli/issue/ids.py,sha256=ndwZINbRE-Ax_1mu6QTaOocVjFxmUXIHLdskGadsK-U,3251
|
|
83
|
+
gitea/cli/issue/list.py,sha256=_QEZ7fhyxD9z_rPIo-w68lW3xgJGVMGba-8wJ126Fic,9869
|
|
83
84
|
gitea/cli/issue/main.py,sha256=LdqQh_zEh-bJK0kTwxIyRDRWi3bXwXBX4UW2RkGOq28,1685
|
|
84
85
|
gitea/cli/issue/dependency/__init__.py,sha256=cbMVSnGnivUIYUcEwjQkaJ-7FmjzPftKYtnp0xjDBvY,52
|
|
85
86
|
gitea/cli/issue/dependency/add.py,sha256=jlfGJt-5H5WF55m5WBPAlWdGdlEurBhIExUicbE34qM,4542
|
|
@@ -116,14 +117,14 @@ gitea/cli/project/create.py,sha256=Ku0zwVgEP4RwvOcu16VUiQluTaK6vRrOZ__i40hiFAo,3
|
|
|
116
117
|
gitea/cli/project/delete.py,sha256=XbGgn9ccD6AAzozHCjjT0PIO2WmV-OngGTgYmkyVbBc,2565
|
|
117
118
|
gitea/cli/project/edit.py,sha256=HYk1ucsjxctmKqb6w3oDp4giX7eZy8tKbY_vcQ7nFj4,3438
|
|
118
119
|
gitea/cli/project/get.py,sha256=Fb8REqjoZkXkPqGIsGz_53c9hNRkF3xxoDOiv6XsXxs,2546
|
|
119
|
-
gitea/cli/project/issues.py,sha256=
|
|
120
|
+
gitea/cli/project/issues.py,sha256=ffwIlW9Xik3zRs00ZFmUd6chm5oDTiNh05Uqdsl_Un4,4249
|
|
120
121
|
gitea/cli/project/list.py,sha256=_VjnD0Tzjk_Pl9Ir-Tfb-J-cO-5P08vBjKEM0erlZEQ,3020
|
|
121
122
|
gitea/cli/project/main.py,sha256=lRswFzhJ3B54IkGT0v2ZaLsvkvVELH-W_zJfSjs019k,3527
|
|
122
123
|
gitea/cli/project/show.py,sha256=C6OTlzjvjfdpS3RF_XTBKaBAdaI4WSKJU2Sdi1kTAF0,5657
|
|
123
124
|
gitea/cli/project/column/__init__.py,sha256=GiQ-qrPCzeUcu-V1YzJrm-dqP2vsSt1e1j4UsXoMSNA,49
|
|
124
125
|
gitea/cli/project/column/create.py,sha256=VnrFYSnsjqBdmzeT5Hn-sDwIRD8uQp2bA4iDZvLL2v4,2989
|
|
125
|
-
gitea/cli/project/column/issues.py,sha256=
|
|
126
|
-
gitea/cli/project/column/list.py,sha256=
|
|
126
|
+
gitea/cli/project/column/issues.py,sha256=jyxYdRDpCIp8q2v3pXX6pqJcWf4-g9O0c_2Hx-aqGo4,4224
|
|
127
|
+
gitea/cli/project/column/list.py,sha256=Ij_ucIlBRJBtQ2anIwBomwhDv_vdBDJr4M4kcbKg6Tk,3434
|
|
127
128
|
gitea/cli/project/issue/__init__.py,sha256=MnZE61XcpwD5MGvOcTZkYdyDe7LLdzLEehuXmrSI6q4,48
|
|
128
129
|
gitea/cli/project/issue/add.py,sha256=uQ72ffrHQt9OGjXsls6HZiUrpLPkx65lrOglEDLhcVc,4373
|
|
129
130
|
gitea/cli/project/issue/move.py,sha256=X4tQ7oBMsCuE2LlUQrRUpO4qvUlGGQYGST6LL9fUxIg,4876
|
|
@@ -145,6 +146,7 @@ gitea/cli/utils/convert.py,sha256=kf2txtS976rM9d7U-NhyqKUQLm7fQjx-y9Qr6fHSLq0,97
|
|
|
145
146
|
gitea/cli/utils/errors.py,sha256=KRKsqTumWTr7OZnjK6LudPv2Rx42WCrDj2RETGMmCa0,2148
|
|
146
147
|
gitea/cli/utils/issue.py,sha256=kaa4Ek7NY8ItR5Ng66jjFs0OLzMo9rAy3dxO4_Q_FG8,52359
|
|
147
148
|
gitea/cli/utils/options.py,sha256=-UXhhSQoAQAYTka4-zIP2Cju6Ku_gGS27THkTrRn0IQ,7585
|
|
149
|
+
gitea/cli/utils/projection.py,sha256=1uhSoYU7rBKvT-_ISL_4B_RlCTfjbS6MFi00cwSsr28,2026
|
|
148
150
|
gitea/cli/watch/__init__.py,sha256=6PWnogslBxaoc7j1GBRalQhF1pwiDofKy24zwiWTjcg,52
|
|
149
151
|
gitea/cli/watch/advance.py,sha256=l3pMuRrvadcVFxPTUDAbv7abFEQ6oZ5XPBrR34OXFp8,9250
|
|
150
152
|
gitea/cli/watch/list.py,sha256=FhRlW1cgfYtrlk-8xj2hqPdzgF6weYPJz2qplwHs37I,7730
|
|
@@ -165,7 +167,7 @@ gitea/issue/__init__.py,sha256=gScB9oA-26poYwUEw1Ce-xx4rmh2GS4uNn94s_3E_Yc,1514
|
|
|
165
167
|
gitea/issue/async_issue.py,sha256=1ME-nN5usqoyCbwtaBmSiHbN-QfHIHFQq2gFbG7A_5U,29500
|
|
166
168
|
gitea/issue/base.py,sha256=Q13QLwxKGozvmd7oPiex9Kw5unsdvCKF-GCvdg0-zJQ,15298
|
|
167
169
|
gitea/issue/issue.py,sha256=wpXoGH9JQps3ypbGyzOkqKacGGnDxXxwFWrPEtORXfw,28894
|
|
168
|
-
gitea/issue/project_column.py,sha256=
|
|
170
|
+
gitea/issue/project_column.py,sha256=WmjeX1T_uVykxpOQmWKEqFQrV_jlXGr4F4X0CTF7vx8,19487
|
|
169
171
|
gitea/label/__init__.py,sha256=cHdpsWsPWOX-q6k1JRzPOFSRafJUj907lmndLntjZjQ,690
|
|
170
172
|
gitea/label/async_label.py,sha256=11UPD9T15Z3_Z2f63a9oxnwy__qDXTrIECZ03S0ecgE,10049
|
|
171
173
|
gitea/label/base.py,sha256=8zD3r7n6iC4d4bnnDgTB8yfuu0GuZQFoNTC6T7xUcQ4,6483
|
|
@@ -210,8 +212,8 @@ gitea/utils/response.py,sha256=dDk-joN6TA3blefFGTxWTs41dHOUapQs3OxoOqiMg6o,5945
|
|
|
210
212
|
gitea/watch/__init__.py,sha256=xeLLpoldkTAJda4WPOVa6NvE1i_X2VcUcgiMFoK9ImU,1422
|
|
211
213
|
gitea/watch/changes.py,sha256=lSeq3-V8IjvcoZFp5teciiezOlQa89vhaWju80JEHO4,14418
|
|
212
214
|
gitea/watch/state.py,sha256=ZmwPnTRavdyIJuXIY0QllVCh56O_p9oPdVzS-pt0oCs,17856
|
|
213
|
-
python_gitea-0.
|
|
214
|
-
python_gitea-0.
|
|
215
|
-
python_gitea-0.
|
|
216
|
-
python_gitea-0.
|
|
217
|
-
python_gitea-0.
|
|
215
|
+
python_gitea-0.13.1.dist-info/METADATA,sha256=HB74DWpFZtxoSBmEdmDoRKpV2RzX-Ctw5m531zif_2g,7634
|
|
216
|
+
python_gitea-0.13.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
217
|
+
python_gitea-0.13.1.dist-info/entry_points.txt,sha256=m5yt-1RMRZRFMj_M-Un5E4eOkM113S9EppAHp0aCzEY,49
|
|
218
|
+
python_gitea-0.13.1.dist-info/licenses/LICENSE,sha256=4dW9zcCxiQKAu4CyNKJTRmvzdJCCHDxbPGuQ-YALpsY,1137
|
|
219
|
+
python_gitea-0.13.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|