python-gitea 0.12.2__py3-none-any.whl → 0.13.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.
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.utils.options import DEPRECATED_INDEX_HELP, ISSUE_ID_HELP, REPOSITORY_REQUIRED_HELP
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
- issue_id: Annotated[int | None, typer.Option("--issue-id", help=ISSUE_ID_HELP)] = None,
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 a specific issue in a repository.
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
- issue_id: The issue number shown in the web UI.
56
- index: The deprecated name of `issue_id`.
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, resolve_issue_id # noqa: PLC0415
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 issue information.
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 the issue is on is resolved from the project's
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="gitea-cli issue get")
87
- target_issue = resolve_issue_id(issue_id=issue_id, index=index, command="gitea-cli issue get")
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
- data, metadata = client.issue.get_issue(
91
- owner=owner,
92
- repository=target_repository,
93
- index=target_issue,
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
- data = resolve_project_column_ids(client=client, owner=owner, repository=target_repository, issue=data)
96
- return data, metadata
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
- execute_api_command(api_call=api_call, base_url=base_url, command_name="gitea-cli issue get")
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="gitea-cli issue list")
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
- return client.issue.list_issues(
137
- owner=owner,
138
- repository=target_repository,
139
- state=state,
140
- labels=labels,
141
- search_string=search_string,
142
- issue_type=issue_type,
143
- milestones=milestones_values,
144
- since=since,
145
- before=before,
146
- created_by=created_by,
147
- assigned_by=assigned_by,
148
- mentioned_by=mentioned_by,
149
- page=page,
150
- limit=limit,
151
- )
152
-
153
- execute_api_command(api_call=api_call, base_url=base_url, command_name="gitea-cli issue list")
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 issue data and metadata.
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
- return client.project.list_project_column_issues(
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")
@@ -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
- return client.project.list_project_columns(
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")
@@ -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]
@@ -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
- for columns, _ in iter_pages(
215
- lambda page: client.project.list_project_columns(
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
- page=page,
220
- limit=PAGE_SIZE,
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.12.2
3
+ Version: 0.13.0
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=qEUnoXioVfvOBU0P4tLaKTtEjonvUHCs1XkUscfJZCY,4066
82
- gitea/cli/issue/list.py,sha256=AjnrA1LwJwtUn596JimUhKSjyMDWRsm9ywBdaxtZSHk,5584
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=fgwM90ckiIcZx8EWKLdFrXJDQrntc0Bzp6rVBYDtwWs,3851
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=j48Pfmg1dxBjnBAdNBdDO5kZciqX2hkkaT8L_NB2TDc,3186
126
- gitea/cli/project/column/list.py,sha256=_cuKmTeN4W-S8ixAXFs1178zPg9p5LJbUsGlSUR8q1g,2988
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=7TpnQcq9-hqc9eEciluplHQmSLiEBkRBr-lH7y6L9s8,15005
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.12.2.dist-info/METADATA,sha256=PgXEdpd3tfl6AKhOdc6MqZ-XrpRqAkb0wluhPpmf9Go,7634
214
- python_gitea-0.12.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
215
- python_gitea-0.12.2.dist-info/entry_points.txt,sha256=m5yt-1RMRZRFMj_M-Un5E4eOkM113S9EppAHp0aCzEY,49
216
- python_gitea-0.12.2.dist-info/licenses/LICENSE,sha256=4dW9zcCxiQKAu4CyNKJTRmvzdJCCHDxbPGuQ-YALpsY,1137
217
- python_gitea-0.12.2.dist-info/RECORD,,
215
+ python_gitea-0.13.0.dist-info/METADATA,sha256=WsXaKSGlrnS0XzU67GBhqUmUQbwhucS_0OwqMgyMuIQ,7634
216
+ python_gitea-0.13.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
217
+ python_gitea-0.13.0.dist-info/entry_points.txt,sha256=m5yt-1RMRZRFMj_M-Un5E4eOkM113S9EppAHp0aCzEY,49
218
+ python_gitea-0.13.0.dist-info/licenses/LICENSE,sha256=4dW9zcCxiQKAu4CyNKJTRmvzdJCCHDxbPGuQ-YALpsY,1137
219
+ python_gitea-0.13.0.dist-info/RECORD,,