kd-cli 2.0.4__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.
- kd/__init__.py +45 -0
- kd/api/__init__.py +15 -0
- kd/api/articles.py +232 -0
- kd/api/http.py +234 -0
- kd/api/hub.py +69 -0
- kd/api/saved_queries.py +76 -0
- kd/api/tags.py +107 -0
- kd/api/youtrack.py +414 -0
- kd/article_marker.py +348 -0
- kd/auth.py +96 -0
- kd/cli/__init__.py +5 -0
- kd/cli/core/__init__.py +25 -0
- kd/cli/core/command_base.py +133 -0
- kd/cli/core/command_parser.py +246 -0
- kd/cli/core/namespaces.py +175 -0
- kd/cli/core/option_registry.py +265 -0
- kd/cli/input.py +42 -0
- kd/cli/main.py +612 -0
- kd/cli/namespaces/__init__.py +38 -0
- kd/cli/namespaces/alias/__init__.py +87 -0
- kd/cli/namespaces/article/__init__.py +843 -0
- kd/cli/namespaces/init.py +42 -0
- kd/cli/namespaces/issue/__init__.py +959 -0
- kd/cli/namespaces/primer/__init__.py +337 -0
- kd/cli/namespaces/project/__init__.py +243 -0
- kd/cli/namespaces/query/__init__.py +465 -0
- kd/cli/namespaces/search/__init__.py +82 -0
- kd/cli/namespaces/system/__init__.py +528 -0
- kd/cli/namespaces/tag/__init__.py +337 -0
- kd/cli/namespaces/time/__init__.py +307 -0
- kd/cli/namespaces/user/__init__.py +99 -0
- kd/cli/output.py +287 -0
- kd/cli/tree.py +65 -0
- kd/client.py +1953 -0
- kd/config.py +853 -0
- kd/connection.py +156 -0
- kd/debug.py +107 -0
- kd/exceptions.py +189 -0
- kd/history.py +169 -0
- kd/models/__init__.py +75 -0
- kd/models/article.py +64 -0
- kd/models/assist.py +47 -0
- kd/models/attachment.py +20 -0
- kd/models/comment.py +25 -0
- kd/models/common.py +39 -0
- kd/models/issue.py +67 -0
- kd/models/project.py +60 -0
- kd/models/saved_query.py +54 -0
- kd/models/search.py +20 -0
- kd/models/tag.py +30 -0
- kd/models/user.py +42 -0
- kd/models/work_item.py +59 -0
- kd/primer.py +231 -0
- kd/query_parser.py +181 -0
- kd/resources/__init__.py +1 -0
- kd/resources/command-synonyms.yaml +19 -0
- kd/resources/config-template.yaml +71 -0
- kd/resources/error-messages.yaml +101 -0
- kd_cli-2.0.4.dist-info/METADATA +149 -0
- kd_cli-2.0.4.dist-info/RECORD +64 -0
- kd_cli-2.0.4.dist-info/WHEEL +5 -0
- kd_cli-2.0.4.dist-info/entry_points.txt +2 -0
- kd_cli-2.0.4.dist-info/licenses/LICENSE +21 -0
- kd_cli-2.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Query namespace: /query
|
|
2
|
+
|
|
3
|
+
Commands: list, show, create, update, delete, issues, articles, validate, assist
|
|
4
|
+
API layer: YouTrack REST API (/api/savedQueries, /api/search/assist)
|
|
5
|
+
|
|
6
|
+
The namespace is named ``/query`` rather than ``/saved-query`` to match the
|
|
7
|
+
single-word cadence of the other namespaces (/tag, /article, /time, /alias).
|
|
8
|
+
Subcommands disambiguate intent — ``list`` and ``show`` inspect saved queries,
|
|
9
|
+
``issues`` and ``articles`` execute one against the respective entity type,
|
|
10
|
+
and ``assist`` calls the search-autocomplete endpoint on a free-form query
|
|
11
|
+
string.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from kd.cli.core.command_base import Command, CommandResult
|
|
20
|
+
from kd.cli.core.namespaces import Namespace
|
|
21
|
+
from kd.cli.core.option_registry import (
|
|
22
|
+
OptionDeclaration,
|
|
23
|
+
OptionLayer,
|
|
24
|
+
ResolvedOptions,
|
|
25
|
+
limit_option,
|
|
26
|
+
project_option,
|
|
27
|
+
)
|
|
28
|
+
from kd.cli.namespaces.issue import _issue_to_row
|
|
29
|
+
from kd.cli.namespaces.tag import _article_to_row
|
|
30
|
+
from kd.exceptions import ValidationError
|
|
31
|
+
from kd.models import AssistSuggestion, QueryValidationResult, SavedQuery
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _saved_query_to_row(sq: SavedQuery) -> dict[str, Any]:
|
|
35
|
+
"""Convert a SavedQuery to a flat dict for table output."""
|
|
36
|
+
return {
|
|
37
|
+
"id": sq.id,
|
|
38
|
+
"name": sq.name,
|
|
39
|
+
"query": sq.query,
|
|
40
|
+
"owner": sq.owner.name if sq.owner and sq.owner.name else "",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _saved_query_to_detail(sq: SavedQuery) -> dict[str, Any]:
|
|
45
|
+
"""Convert a SavedQuery to a detail dict for show/create/update output."""
|
|
46
|
+
return {
|
|
47
|
+
"id": sq.id,
|
|
48
|
+
"name": sq.name,
|
|
49
|
+
"query": sq.query,
|
|
50
|
+
"owner": sq.owner.name if sq.owner and sq.owner.name else "",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _suggestion_to_row(s: AssistSuggestion) -> dict[str, Any]:
|
|
55
|
+
"""Convert an AssistSuggestion to a flat dict for table output."""
|
|
56
|
+
return {
|
|
57
|
+
"option": s.option,
|
|
58
|
+
"description": s.description or "",
|
|
59
|
+
"prefix": s.prefix or "",
|
|
60
|
+
"suffix": s.suffix or "",
|
|
61
|
+
"caret": s.completion_start if s.completion_start is not None else "",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _warn_invalid_query(result: QueryValidationResult, name: str) -> None:
|
|
66
|
+
"""Print a validation warning to stderr if the query is invalid."""
|
|
67
|
+
if result.valid:
|
|
68
|
+
return
|
|
69
|
+
print(f"Warning: query syntax error — {result.error}", file=sys.stderr)
|
|
70
|
+
if result.suggestion:
|
|
71
|
+
print(f"Hint: {result.suggestion}", file=sys.stderr)
|
|
72
|
+
print(f'Hint: delete with: kd /query delete "{name}"', file=sys.stderr)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- Commands ---
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class QueryListCommand(Command):
|
|
79
|
+
"""List saved queries."""
|
|
80
|
+
|
|
81
|
+
def __init__(self) -> None:
|
|
82
|
+
super().__init__(
|
|
83
|
+
"list",
|
|
84
|
+
"List saved queries",
|
|
85
|
+
requires_client=True,
|
|
86
|
+
usage="kd /query list [--mine] [--limit N]",
|
|
87
|
+
long_description=(
|
|
88
|
+
"Lists saved queries visible to the authenticated user. "
|
|
89
|
+
"Pass --mine to restrict to queries you own."
|
|
90
|
+
),
|
|
91
|
+
examples=[
|
|
92
|
+
"kd /query list",
|
|
93
|
+
"kd /query list --mine",
|
|
94
|
+
"kd /query list --limit 100",
|
|
95
|
+
],
|
|
96
|
+
see_also=["/query show", "/query issues", "/query articles"],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _register_options(self) -> None:
|
|
100
|
+
self.register_option(
|
|
101
|
+
OptionDeclaration(
|
|
102
|
+
name="mine",
|
|
103
|
+
layer=OptionLayer.COMMAND,
|
|
104
|
+
type=bool,
|
|
105
|
+
default=False,
|
|
106
|
+
help_text="Only queries owned by the current user",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
self.register_option(limit_option())
|
|
110
|
+
|
|
111
|
+
def execute(
|
|
112
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
113
|
+
) -> CommandResult:
|
|
114
|
+
limit = options.command.get("limit", 50)
|
|
115
|
+
mine = options.command.get("mine", False)
|
|
116
|
+
owner = "me" if mine else None
|
|
117
|
+
queries = client.queries.list(owner=owner, top=limit)
|
|
118
|
+
return [_saved_query_to_row(sq) for sq in queries]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class QueryShowCommand(Command):
|
|
122
|
+
"""Show saved query details."""
|
|
123
|
+
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
super().__init__(
|
|
126
|
+
"show",
|
|
127
|
+
"Show saved query details",
|
|
128
|
+
requires_client=True,
|
|
129
|
+
usage="kd /query show NAME_OR_ID",
|
|
130
|
+
long_description=(
|
|
131
|
+
"Resolves NAME_OR_ID to a saved query. Names are matched "
|
|
132
|
+
"case-insensitively; use the ID (e.g. 11-24) to disambiguate "
|
|
133
|
+
"duplicates."
|
|
134
|
+
),
|
|
135
|
+
examples=[
|
|
136
|
+
'kd /query show "Assigned to me"',
|
|
137
|
+
"kd /query show 11-24",
|
|
138
|
+
],
|
|
139
|
+
see_also=["/query list", "/query issues", "/query articles"],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def execute(
|
|
143
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
144
|
+
) -> CommandResult:
|
|
145
|
+
if not args:
|
|
146
|
+
raise ValidationError("Usage: kd /query show NAME_OR_ID")
|
|
147
|
+
sq = client.queries.get(args[0])
|
|
148
|
+
return _saved_query_to_detail(sq)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class QueryCreateCommand(Command):
|
|
152
|
+
"""Create a saved query."""
|
|
153
|
+
|
|
154
|
+
def __init__(self) -> None:
|
|
155
|
+
super().__init__(
|
|
156
|
+
"create",
|
|
157
|
+
"Create a saved query",
|
|
158
|
+
requires_client=True,
|
|
159
|
+
usage='kd /query create NAME "QUERY_TEXT"',
|
|
160
|
+
long_description=(
|
|
161
|
+
"Creates a personal saved query. Sharing settings default to "
|
|
162
|
+
"owner-only; other users can be granted access via the "
|
|
163
|
+
"YouTrack web UI.\n\n"
|
|
164
|
+
"The query is validated after saving by executing a dry-run "
|
|
165
|
+
"search. If the syntax is invalid, a warning is printed but "
|
|
166
|
+
"the query is still saved.\n\n"
|
|
167
|
+
"Syntax tip: values containing colons, spaces, or braces must "
|
|
168
|
+
"be wrapped in braces — e.g. tag: {type:case} instead of "
|
|
169
|
+
"tag: type:case."
|
|
170
|
+
),
|
|
171
|
+
examples=[
|
|
172
|
+
'kd /query create "My Bugs" "for: me Type: Bug #Unresolved"',
|
|
173
|
+
'kd /query create "Sprint 47" "Sprint: {Sprint 47}"',
|
|
174
|
+
],
|
|
175
|
+
see_also=["/query list", "/query delete"],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def execute(
|
|
179
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
180
|
+
) -> CommandResult:
|
|
181
|
+
if len(args) < 2:
|
|
182
|
+
raise ValidationError('Usage: kd /query create NAME "QUERY_TEXT"')
|
|
183
|
+
name, query_text = args[0], args[1]
|
|
184
|
+
sq = client.queries.create(name, query_text)
|
|
185
|
+
vr = client.queries.validate(query_text)
|
|
186
|
+
_warn_invalid_query(vr, name)
|
|
187
|
+
return _saved_query_to_detail(sq)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class QueryUpdateCommand(Command):
|
|
191
|
+
"""Update a saved query."""
|
|
192
|
+
|
|
193
|
+
def __init__(self) -> None:
|
|
194
|
+
super().__init__(
|
|
195
|
+
"update",
|
|
196
|
+
"Update a saved query",
|
|
197
|
+
requires_client=True,
|
|
198
|
+
usage="kd /query update NAME_OR_ID [--name NEW_NAME] [--query NEW_QUERY]",
|
|
199
|
+
long_description=(
|
|
200
|
+
"Partial update — pass whichever of --name / --query you want "
|
|
201
|
+
"to change. At least one is required.\n\n"
|
|
202
|
+
"When --query is provided, the new query text is validated by "
|
|
203
|
+
"executing a dry-run search. A warning is printed if the "
|
|
204
|
+
"syntax is invalid.\n\n"
|
|
205
|
+
"Syntax tip: values containing colons, spaces, or braces must "
|
|
206
|
+
"be wrapped in braces — e.g. tag: {type:case} instead of "
|
|
207
|
+
"tag: type:case."
|
|
208
|
+
),
|
|
209
|
+
examples=[
|
|
210
|
+
'kd /query update "My Bugs" --name "My Bugs (archived)"',
|
|
211
|
+
'kd /query update 11-24 --query "project: FB #Unresolved"',
|
|
212
|
+
'kd /query update 11-24 --name "New" --query "project: FB"',
|
|
213
|
+
],
|
|
214
|
+
see_also=["/query show", "/query create"],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _register_options(self) -> None:
|
|
218
|
+
self.register_option(
|
|
219
|
+
OptionDeclaration(
|
|
220
|
+
name="name",
|
|
221
|
+
layer=OptionLayer.COMMAND,
|
|
222
|
+
help_text="New name",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
self.register_option(
|
|
226
|
+
OptionDeclaration(
|
|
227
|
+
name="query",
|
|
228
|
+
layer=OptionLayer.COMMAND,
|
|
229
|
+
help_text="New query text",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def execute(
|
|
234
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
235
|
+
) -> CommandResult:
|
|
236
|
+
if not args:
|
|
237
|
+
raise ValidationError("Usage: kd /query update NAME_OR_ID [--name ...] [--query ...]")
|
|
238
|
+
new_name = options.command.get("name")
|
|
239
|
+
new_query = options.command.get("query")
|
|
240
|
+
if new_name is None and new_query is None:
|
|
241
|
+
raise ValidationError("Provide at least one of --name or --query to update.")
|
|
242
|
+
sq = client.queries.update(args[0], name=new_name, query=new_query)
|
|
243
|
+
if new_query is not None:
|
|
244
|
+
vr = client.queries.validate(new_query)
|
|
245
|
+
_warn_invalid_query(vr, sq.name)
|
|
246
|
+
return _saved_query_to_detail(sq)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class QueryDeleteCommand(Command):
|
|
250
|
+
"""Delete a saved query."""
|
|
251
|
+
|
|
252
|
+
def __init__(self) -> None:
|
|
253
|
+
super().__init__(
|
|
254
|
+
"delete",
|
|
255
|
+
"Delete a saved query",
|
|
256
|
+
requires_client=True,
|
|
257
|
+
usage="kd /query delete NAME_OR_ID",
|
|
258
|
+
examples=[
|
|
259
|
+
'kd /query delete "My Bugs"',
|
|
260
|
+
"kd /query delete 11-24",
|
|
261
|
+
],
|
|
262
|
+
see_also=["/query list", "/query create"],
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def execute(
|
|
266
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
267
|
+
) -> CommandResult:
|
|
268
|
+
if not args:
|
|
269
|
+
raise ValidationError("Usage: kd /query delete NAME_OR_ID")
|
|
270
|
+
target = args[0]
|
|
271
|
+
client.queries.delete(target)
|
|
272
|
+
return {"ok": True, "action": "delete", "query": target}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class QueryIssuesCommand(Command):
|
|
276
|
+
"""Run a saved query and return matching issues."""
|
|
277
|
+
|
|
278
|
+
def __init__(self) -> None:
|
|
279
|
+
super().__init__(
|
|
280
|
+
"issues",
|
|
281
|
+
"Execute a saved query against issues",
|
|
282
|
+
requires_client=True,
|
|
283
|
+
usage="kd /query issues NAME_OR_ID [--project P] [--limit N]",
|
|
284
|
+
long_description=(
|
|
285
|
+
"Resolves the saved query, fetches its query text, and runs "
|
|
286
|
+
"it through /issue list. Use --project to scope results to a "
|
|
287
|
+
"specific project. Output columns match `kd /issue list`."
|
|
288
|
+
),
|
|
289
|
+
examples=[
|
|
290
|
+
'kd /query issues "My Bugs"',
|
|
291
|
+
'kd /query issues "Sprint 47" --limit 100',
|
|
292
|
+
'kd /query issues "My Bugs" --project FB',
|
|
293
|
+
],
|
|
294
|
+
see_also=["/query list", "/query articles", "/issue list"],
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _register_options(self) -> None:
|
|
298
|
+
self.register_option(project_option(help_text="Scope results to a project"))
|
|
299
|
+
self.register_option(limit_option())
|
|
300
|
+
|
|
301
|
+
def execute(
|
|
302
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
303
|
+
) -> CommandResult:
|
|
304
|
+
if not args:
|
|
305
|
+
raise ValidationError("Usage: kd /query issues NAME_OR_ID")
|
|
306
|
+
limit = options.command.get("limit", 50)
|
|
307
|
+
project = options.command.get("project")
|
|
308
|
+
issues = client.queries.run_issues(args[0], top=limit, project=project)
|
|
309
|
+
return [_issue_to_row(i) for i in issues]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class QueryArticlesCommand(Command):
|
|
313
|
+
"""Run a saved query and return matching articles."""
|
|
314
|
+
|
|
315
|
+
def __init__(self) -> None:
|
|
316
|
+
super().__init__(
|
|
317
|
+
"articles",
|
|
318
|
+
"Execute a saved query against articles",
|
|
319
|
+
requires_client=True,
|
|
320
|
+
usage="kd /query articles NAME_OR_ID [--project P] [--limit N]",
|
|
321
|
+
long_description=(
|
|
322
|
+
"Resolves the saved query, parses project and tag fields from "
|
|
323
|
+
"the query text, and finds articles matching those tags. "
|
|
324
|
+
"Use --project to scope or override the project parsed from "
|
|
325
|
+
"the query text. Only project: and tag: fields are supported — "
|
|
326
|
+
"queries with issue-specific fields (state, assignee, priority, "
|
|
327
|
+
"etc.) are rejected with an error.\n\n"
|
|
328
|
+
"Output columns match `kd /tag articles`."
|
|
329
|
+
),
|
|
330
|
+
examples=[
|
|
331
|
+
'kd /query articles "CMCSA Cases"',
|
|
332
|
+
'kd /query articles "Tagged Guides" --limit 100',
|
|
333
|
+
'kd /query articles "Tagged Guides" --project CMCSA',
|
|
334
|
+
],
|
|
335
|
+
see_also=["/query list", "/query issues", "/tag articles"],
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _register_options(self) -> None:
|
|
339
|
+
self.register_option(project_option(help_text="Scope results to a project"))
|
|
340
|
+
self.register_option(limit_option())
|
|
341
|
+
|
|
342
|
+
def execute(
|
|
343
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
344
|
+
) -> CommandResult:
|
|
345
|
+
if not args:
|
|
346
|
+
raise ValidationError("Usage: kd /query articles NAME_OR_ID")
|
|
347
|
+
limit = options.command.get("limit", 50)
|
|
348
|
+
project = options.command.get("project")
|
|
349
|
+
articles = client.queries.run_articles(args[0], top=limit, project=project)
|
|
350
|
+
return [_article_to_row(a) for a in articles]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class QueryValidateCommand(Command):
|
|
354
|
+
"""Validate saved query syntax."""
|
|
355
|
+
|
|
356
|
+
def __init__(self) -> None:
|
|
357
|
+
super().__init__(
|
|
358
|
+
"validate",
|
|
359
|
+
"Validate saved query syntax against the search API",
|
|
360
|
+
requires_client=True,
|
|
361
|
+
usage="kd /query validate [NAME_OR_ID]",
|
|
362
|
+
long_description=(
|
|
363
|
+
"Validates query syntax by executing a dry-run search. "
|
|
364
|
+
"With no argument, validates all saved queries and reports "
|
|
365
|
+
"which ones have syntax errors. With a name or ID, validates "
|
|
366
|
+
"a single saved query."
|
|
367
|
+
),
|
|
368
|
+
examples=[
|
|
369
|
+
"kd /query validate",
|
|
370
|
+
'kd /query validate "My Bugs"',
|
|
371
|
+
"kd /query validate 11-24",
|
|
372
|
+
],
|
|
373
|
+
see_also=["/query list", "/query create"],
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def execute(
|
|
377
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
378
|
+
) -> CommandResult:
|
|
379
|
+
if args:
|
|
380
|
+
sq = client.queries.get(args[0])
|
|
381
|
+
vr = client.queries.validate(sq.query)
|
|
382
|
+
return _validation_to_row(sq, vr)
|
|
383
|
+
queries = client.queries.list(top=500)
|
|
384
|
+
rows: list[dict[str, Any]] = []
|
|
385
|
+
for sq in queries:
|
|
386
|
+
vr = client.queries.validate(sq.query)
|
|
387
|
+
rows.append(_validation_to_row(sq, vr))
|
|
388
|
+
return rows
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _validation_to_row(sq: SavedQuery, vr: QueryValidationResult) -> dict[str, Any]:
|
|
392
|
+
"""Convert a validation result to a flat dict for output."""
|
|
393
|
+
return {
|
|
394
|
+
"name": sq.name,
|
|
395
|
+
"query": sq.query,
|
|
396
|
+
"status": "ok" if vr.valid else "error",
|
|
397
|
+
"error": vr.error or "",
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class QueryAssistCommand(Command):
|
|
402
|
+
"""Fetch search-query autocomplete suggestions."""
|
|
403
|
+
|
|
404
|
+
def __init__(self) -> None:
|
|
405
|
+
super().__init__(
|
|
406
|
+
"assist",
|
|
407
|
+
"Autocomplete a YouTrack search query",
|
|
408
|
+
requires_client=True,
|
|
409
|
+
usage='kd /query assist "QUERY_TEXT" [--caret N]',
|
|
410
|
+
long_description=(
|
|
411
|
+
"Wraps POST /api/search/assist. --caret defaults to the end "
|
|
412
|
+
"of QUERY_TEXT so typical use (tab-completion at the tail) "
|
|
413
|
+
"works without specifying it."
|
|
414
|
+
),
|
|
415
|
+
examples=[
|
|
416
|
+
'kd /query assist "project: "',
|
|
417
|
+
'kd /query assist "for: " --caret 5',
|
|
418
|
+
'kd /query assist "Assignee: ad"',
|
|
419
|
+
],
|
|
420
|
+
see_also=["/query issues", "/query create"],
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _register_options(self) -> None:
|
|
424
|
+
self.register_option(
|
|
425
|
+
OptionDeclaration(
|
|
426
|
+
name="caret",
|
|
427
|
+
layer=OptionLayer.COMMAND,
|
|
428
|
+
type=int,
|
|
429
|
+
help_text="Caret position (defaults to end of query)",
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def execute(
|
|
434
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
435
|
+
) -> CommandResult:
|
|
436
|
+
if not args:
|
|
437
|
+
raise ValidationError('Usage: kd /query assist "QUERY_TEXT" [--caret N]')
|
|
438
|
+
query_text = args[0]
|
|
439
|
+
caret = options.command.get("caret")
|
|
440
|
+
if caret is None:
|
|
441
|
+
caret = len(query_text)
|
|
442
|
+
response = client.assist.search(query_text, caret=caret)
|
|
443
|
+
return [_suggestion_to_row(s) for s in response.suggestions]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# --- Namespace factory ---
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def create_query_namespace() -> Namespace:
|
|
450
|
+
"""Build the /query namespace."""
|
|
451
|
+
ns = Namespace(
|
|
452
|
+
"query",
|
|
453
|
+
"Saved queries and search autocomplete",
|
|
454
|
+
path="query",
|
|
455
|
+
)
|
|
456
|
+
ns.register_command("list", QueryListCommand())
|
|
457
|
+
ns.register_command("show", QueryShowCommand())
|
|
458
|
+
ns.register_command("create", QueryCreateCommand())
|
|
459
|
+
ns.register_command("update", QueryUpdateCommand())
|
|
460
|
+
ns.register_command("delete", QueryDeleteCommand())
|
|
461
|
+
ns.register_command("issues", QueryIssuesCommand())
|
|
462
|
+
ns.register_command("articles", QueryArticlesCommand())
|
|
463
|
+
ns.register_command("validate", QueryValidateCommand())
|
|
464
|
+
ns.register_command("assist", QueryAssistCommand())
|
|
465
|
+
return ns
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Search command: /search
|
|
2
|
+
|
|
3
|
+
Cross-entity aggregated search across issues, users, tags, projects,
|
|
4
|
+
and articles. Backed by composed calls to multiple YouTrack REST API
|
|
5
|
+
endpoints (client-side aggregation, not a single upstream endpoint).
|
|
6
|
+
|
|
7
|
+
Registered as a root command (not a namespace) so that the first
|
|
8
|
+
positional argument is the search query, not a subcommand name.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from kd.cli.core.command_base import Command, CommandResult
|
|
16
|
+
from kd.cli.core.option_registry import (
|
|
17
|
+
OptionDeclaration,
|
|
18
|
+
OptionLayer,
|
|
19
|
+
ResolvedOptions,
|
|
20
|
+
limit_option,
|
|
21
|
+
project_option,
|
|
22
|
+
)
|
|
23
|
+
from kd.exceptions import ValidationError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SearchCommand(Command):
|
|
27
|
+
"""Cross-entity search."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
super().__init__(
|
|
31
|
+
"search",
|
|
32
|
+
"Search across entity types",
|
|
33
|
+
requires_client=True,
|
|
34
|
+
usage="kd /search QUERY [--types TYPE,...] [--project P] [--limit N]",
|
|
35
|
+
long_description=(
|
|
36
|
+
"Aggregated search across issues, users, tags, projects, and articles. "
|
|
37
|
+
"Issue and user searches use server-side query APIs. Tag, project, and "
|
|
38
|
+
"article searches use client-side substring matching."
|
|
39
|
+
),
|
|
40
|
+
examples=[
|
|
41
|
+
'kd /search "router"',
|
|
42
|
+
'kd /search "router" --types issue,article',
|
|
43
|
+
'kd /search "alice" --types user',
|
|
44
|
+
'kd /search "dns" --project FB',
|
|
45
|
+
'kd /search "router" --limit 10',
|
|
46
|
+
],
|
|
47
|
+
see_also=["/issue list"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _register_options(self) -> None:
|
|
51
|
+
self.register_option(
|
|
52
|
+
OptionDeclaration(
|
|
53
|
+
name="types",
|
|
54
|
+
layer=OptionLayer.COMMAND,
|
|
55
|
+
type=str,
|
|
56
|
+
default=None,
|
|
57
|
+
help_text="Comma-separated entity types: issue,user,tag,project,article",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
self.register_option(project_option(help_text="Scope issues and articles to a project"))
|
|
61
|
+
self.register_option(limit_option())
|
|
62
|
+
|
|
63
|
+
def execute(
|
|
64
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
65
|
+
) -> CommandResult:
|
|
66
|
+
if not args:
|
|
67
|
+
raise ValidationError("Usage: kd /search QUERY")
|
|
68
|
+
query = args[0]
|
|
69
|
+
types_raw = options.command.get("types")
|
|
70
|
+
types = types_raw.split(",") if types_raw else None
|
|
71
|
+
project = options.command.get("project")
|
|
72
|
+
limit = options.command.get("limit", 50)
|
|
73
|
+
results = client.search.search(query, types=types, project=project, limit=limit)
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
"type": r.type,
|
|
77
|
+
"id": r.id,
|
|
78
|
+
"title": r.title,
|
|
79
|
+
"project": r.project,
|
|
80
|
+
}
|
|
81
|
+
for r in results
|
|
82
|
+
]
|