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.
Files changed (64) hide show
  1. kd/__init__.py +45 -0
  2. kd/api/__init__.py +15 -0
  3. kd/api/articles.py +232 -0
  4. kd/api/http.py +234 -0
  5. kd/api/hub.py +69 -0
  6. kd/api/saved_queries.py +76 -0
  7. kd/api/tags.py +107 -0
  8. kd/api/youtrack.py +414 -0
  9. kd/article_marker.py +348 -0
  10. kd/auth.py +96 -0
  11. kd/cli/__init__.py +5 -0
  12. kd/cli/core/__init__.py +25 -0
  13. kd/cli/core/command_base.py +133 -0
  14. kd/cli/core/command_parser.py +246 -0
  15. kd/cli/core/namespaces.py +175 -0
  16. kd/cli/core/option_registry.py +265 -0
  17. kd/cli/input.py +42 -0
  18. kd/cli/main.py +612 -0
  19. kd/cli/namespaces/__init__.py +38 -0
  20. kd/cli/namespaces/alias/__init__.py +87 -0
  21. kd/cli/namespaces/article/__init__.py +843 -0
  22. kd/cli/namespaces/init.py +42 -0
  23. kd/cli/namespaces/issue/__init__.py +959 -0
  24. kd/cli/namespaces/primer/__init__.py +337 -0
  25. kd/cli/namespaces/project/__init__.py +243 -0
  26. kd/cli/namespaces/query/__init__.py +465 -0
  27. kd/cli/namespaces/search/__init__.py +82 -0
  28. kd/cli/namespaces/system/__init__.py +528 -0
  29. kd/cli/namespaces/tag/__init__.py +337 -0
  30. kd/cli/namespaces/time/__init__.py +307 -0
  31. kd/cli/namespaces/user/__init__.py +99 -0
  32. kd/cli/output.py +287 -0
  33. kd/cli/tree.py +65 -0
  34. kd/client.py +1953 -0
  35. kd/config.py +853 -0
  36. kd/connection.py +156 -0
  37. kd/debug.py +107 -0
  38. kd/exceptions.py +189 -0
  39. kd/history.py +169 -0
  40. kd/models/__init__.py +75 -0
  41. kd/models/article.py +64 -0
  42. kd/models/assist.py +47 -0
  43. kd/models/attachment.py +20 -0
  44. kd/models/comment.py +25 -0
  45. kd/models/common.py +39 -0
  46. kd/models/issue.py +67 -0
  47. kd/models/project.py +60 -0
  48. kd/models/saved_query.py +54 -0
  49. kd/models/search.py +20 -0
  50. kd/models/tag.py +30 -0
  51. kd/models/user.py +42 -0
  52. kd/models/work_item.py +59 -0
  53. kd/primer.py +231 -0
  54. kd/query_parser.py +181 -0
  55. kd/resources/__init__.py +1 -0
  56. kd/resources/command-synonyms.yaml +19 -0
  57. kd/resources/config-template.yaml +71 -0
  58. kd/resources/error-messages.yaml +101 -0
  59. kd_cli-2.0.4.dist-info/METADATA +149 -0
  60. kd_cli-2.0.4.dist-info/RECORD +64 -0
  61. kd_cli-2.0.4.dist-info/WHEEL +5 -0
  62. kd_cli-2.0.4.dist-info/entry_points.txt +2 -0
  63. kd_cli-2.0.4.dist-info/licenses/LICENSE +21 -0
  64. kd_cli-2.0.4.dist-info/top_level.txt +1 -0
kd/models/article.py ADDED
@@ -0,0 +1,64 @@
1
+ """Article models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from kd.models.common import IdEntity, NamedEntity
8
+ from kd.models.tag import Tag
9
+
10
+
11
+ class ArticleRef(IdEntity):
12
+ """Lightweight cross-reference to an article.
13
+
14
+ Carries ``id_readable`` because articles are addressed by their
15
+ readable id (e.g. ``FB-A-1``) in the CLI. ``NamedEntity`` only has
16
+ ``id`` and ``name`` and is reused for leaders, reporters, authors —
17
+ entities that do not have a human-readable id — so a dedicated type
18
+ keeps the shared base clean.
19
+ """
20
+
21
+ model_config = ConfigDict(populate_by_name=True)
22
+
23
+ id_readable: str | None = Field(default=None, alias="idReadable")
24
+ name: str | None = None
25
+
26
+
27
+ class Article(IdEntity):
28
+ """Knowledge base article.
29
+
30
+ The ``content`` field is populated only when detail fields are
31
+ requested (get, create, update responses). List responses omit it
32
+ to keep payloads lightweight.
33
+ """
34
+
35
+ id_readable: str = Field(alias="idReadable")
36
+ summary: str
37
+ content: str | None = None
38
+ project: NamedEntity | None = None
39
+ reporter: NamedEntity | None = None
40
+ created: int | None = None
41
+ updated: int | None = None
42
+ has_children: bool = Field(default=False, alias="hasChildren")
43
+ parent_article: ArticleRef | None = Field(default=None, alias="parentArticle")
44
+ tags: list[Tag] = Field(default_factory=list)
45
+
46
+
47
+ class ArticleCreate(BaseModel):
48
+ """Input for creating an article."""
49
+
50
+ model_config = ConfigDict(populate_by_name=True)
51
+
52
+ project: IdEntity
53
+ summary: str
54
+ content: str | None = None
55
+ parent_article: IdEntity | None = Field(default=None, alias="parentArticle")
56
+
57
+
58
+ class ArticleUpdate(BaseModel):
59
+ """Input for updating an article."""
60
+
61
+ model_config = ConfigDict(populate_by_name=True)
62
+
63
+ summary: str | None = None
64
+ content: str | None = None
kd/models/assist.py ADDED
@@ -0,0 +1,47 @@
1
+ """Query and command assist models.
2
+
3
+ Used for ``/api/search/assist`` and ``/api/commands/assist`` — YouTrack's
4
+ autocomplete helpers for search queries and issue-command strings.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+
12
+ class AssistSuggestion(BaseModel):
13
+ """One suggestion returned by an assist endpoint.
14
+
15
+ ``option`` is the replacement string; the caret position fields describe
16
+ where the replacement should be inserted and which part of the current
17
+ input it matched. ``prefix``/``suffix`` are the characters to wrap around
18
+ ``option`` when inserting (e.g. ``{`` / ``}`` for project keys with
19
+ spaces).
20
+ """
21
+
22
+ model_config = ConfigDict(populate_by_name=True)
23
+
24
+ option: str
25
+ description: str | None = None
26
+ prefix: str | None = None
27
+ suffix: str | None = None
28
+ completion_start: int | None = Field(default=None, alias="completionStart")
29
+ completion_end: int | None = Field(default=None, alias="completionEnd")
30
+ matching_start: int | None = Field(default=None, alias="matchingStart")
31
+ matching_end: int | None = Field(default=None, alias="matchingEnd")
32
+ group: str | None = None
33
+
34
+
35
+ class AssistResponse(BaseModel):
36
+ """Envelope returned by both assist endpoints.
37
+
38
+ ``query`` and ``caret`` echo the caller's input when requested in the
39
+ field set; ``commands/assist`` does not always return ``caret``. Both are
40
+ optional to accommodate the partial shape.
41
+ """
42
+
43
+ model_config = ConfigDict(populate_by_name=True)
44
+
45
+ query: str | None = None
46
+ caret: int | None = None
47
+ suggestions: list[AssistSuggestion] = Field(default_factory=list)
@@ -0,0 +1,20 @@
1
+ """Attachment models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import Field
6
+
7
+ from kd.models.common import IdEntity, NamedEntity
8
+
9
+
10
+ class Attachment(IdEntity):
11
+ """Issue or article attachment metadata."""
12
+
13
+ name: str
14
+ size: int | None = None
15
+ mime_type: str | None = Field(default=None, alias="mimeType")
16
+ extension: str | None = None
17
+ author: NamedEntity | None = None
18
+ created: int | None = None
19
+ updated: int | None = None
20
+ url: str | None = None
kd/models/comment.py ADDED
@@ -0,0 +1,25 @@
1
+ """Comment models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from kd.models.common import IdEntity, NamedEntity
8
+
9
+
10
+ class Comment(IdEntity):
11
+ """Issue or article comment."""
12
+
13
+ model_config = ConfigDict(populate_by_name=True)
14
+
15
+ text: str
16
+ author: NamedEntity | None = None
17
+ created: int | None = None
18
+ updated: int | None = None
19
+ pinned: bool | None = None
20
+
21
+
22
+ class CommentCreate(BaseModel):
23
+ """Input for adding a comment."""
24
+
25
+ text: str
kd/models/common.py ADDED
@@ -0,0 +1,39 @@
1
+ """Shared model types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class IdEntity(BaseModel):
9
+ """Base for all entities with an id field."""
10
+
11
+ model_config = ConfigDict(populate_by_name=True)
12
+
13
+ id: str
14
+
15
+
16
+ class NamedEntity(IdEntity):
17
+ """Entity with id and name — used for nested references (leader, reporter, author)."""
18
+
19
+ name: str | None = None
20
+
21
+
22
+ class FieldSet:
23
+ """Helper for building YouTrack's explicit field selection strings.
24
+
25
+ Usage::
26
+
27
+ str(FieldSet("id", "name", leader=FieldSet("id", "name")))
28
+ # => "id,name,leader(id,name)"
29
+ """
30
+
31
+ def __init__(self, *fields: str, **nested: FieldSet) -> None:
32
+ self._fields = fields
33
+ self._nested = nested
34
+
35
+ def __str__(self) -> str:
36
+ parts: list[str] = list(self._fields)
37
+ for name, subset in self._nested.items():
38
+ parts.append(f"{name}({subset})")
39
+ return ",".join(parts)
kd/models/issue.py ADDED
@@ -0,0 +1,67 @@
1
+ """Issue models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from kd.models.attachment import Attachment
10
+ from kd.models.comment import Comment
11
+ from kd.models.common import IdEntity, NamedEntity
12
+
13
+
14
+ class CustomField(BaseModel):
15
+ """Custom field value representation.
16
+
17
+ ``value`` is Any because YouTrack custom fields can be dicts with
18
+ a ``name`` key, primitives, or lists depending on field type.
19
+ ``field_type`` serializes as ``$type`` — required by YouTrack for
20
+ create/update operations on enum custom fields.
21
+ """
22
+
23
+ model_config = ConfigDict(populate_by_name=True)
24
+
25
+ name: str
26
+ value: Any = None
27
+ field_type: str | None = Field(default=None, alias="$type")
28
+
29
+
30
+ class Issue(IdEntity):
31
+ """Full issue representation.
32
+
33
+ The ``comments``, ``links``, and ``attachments`` fields are populated
34
+ only when a rich field set is requested from the API.
35
+ """
36
+
37
+ id_readable: str = Field(alias="idReadable")
38
+ summary: str
39
+ description: str | None = None
40
+ project: NamedEntity | None = None
41
+ reporter: NamedEntity | None = None
42
+ created: int | None = None
43
+ custom_fields: list[CustomField] = Field(default_factory=list, alias="customFields")
44
+ comments: list[Comment] | None = None
45
+ links: list[dict[str, Any]] | None = None
46
+ attachments: list[Attachment] | None = None
47
+
48
+
49
+ class IssueCreate(BaseModel):
50
+ """Input for creating an issue."""
51
+
52
+ model_config = ConfigDict(populate_by_name=True)
53
+
54
+ project: IdEntity
55
+ summary: str
56
+ description: str | None = None
57
+ custom_fields: list[CustomField] | None = Field(default=None, alias="customFields")
58
+
59
+
60
+ class IssueUpdate(BaseModel):
61
+ """Input for updating an issue."""
62
+
63
+ model_config = ConfigDict(populate_by_name=True)
64
+
65
+ summary: str | None = None
66
+ description: str | None = None
67
+ custom_fields: list[CustomField] | None = Field(default=None, alias="customFields")
kd/models/project.py ADDED
@@ -0,0 +1,60 @@
1
+ """Project models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from kd.models.common import IdEntity, NamedEntity
8
+
9
+
10
+ class Project(IdEntity):
11
+ """Full project representation."""
12
+
13
+ name: str
14
+ short_name: str = Field(alias="shortName")
15
+ description: str | None = None
16
+ archived: bool = False
17
+ ring_id: str | None = Field(default=None, alias="ringId")
18
+ leader: NamedEntity | None = None
19
+
20
+
21
+ class BundleValue(BaseModel):
22
+ """A single allowed value within an enum or state bundle."""
23
+
24
+ model_config = ConfigDict(populate_by_name=True)
25
+
26
+ name: str
27
+ ordinal: int = 0
28
+ is_resolved: bool | None = Field(default=None, alias="isResolved")
29
+
30
+
31
+ class ProjectCustomField(BaseModel):
32
+ """Custom field attached to a project (flattened from nested API shape).
33
+
34
+ Constructed by the SDK from the raw API response, not parsed directly.
35
+ """
36
+
37
+ name: str
38
+ field_type_id: str
39
+ project_field_type: str
40
+ can_be_empty: bool = True
41
+ empty_field_text: str | None = None
42
+ bundle_values: list[BundleValue] = Field(default_factory=list)
43
+
44
+
45
+ class ProjectCreate(BaseModel):
46
+ """Input for creating a project."""
47
+
48
+ model_config = ConfigDict(populate_by_name=True)
49
+
50
+ name: str
51
+ short_name: str = Field(alias="shortName")
52
+ leader: IdEntity | None = None # Create only needs id
53
+
54
+
55
+ class TimeTrackingSettings(BaseModel):
56
+ """Time tracking configuration per project."""
57
+
58
+ model_config = ConfigDict(populate_by_name=True)
59
+
60
+ enabled: bool = False
@@ -0,0 +1,54 @@
1
+ """Saved query models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from kd.models.common import IdEntity, NamedEntity
8
+
9
+
10
+ class SavedQuery(IdEntity):
11
+ """YouTrack saved query (saved search).
12
+
13
+ Minimal field set: id, name, query text, owner reference. Sharing settings
14
+ (``readSharingSettings`` / ``updateSharingSettings``) are intentionally
15
+ excluded from the default field parameter — personal CRUD works without
16
+ them, and modelling them pulls in a permissions surface that belongs to
17
+ a future milestone.
18
+ """
19
+
20
+ name: str
21
+ query: str
22
+ owner: NamedEntity | None = None
23
+
24
+
25
+ class SavedQueryCreate(BaseModel):
26
+ """Input for creating a saved query."""
27
+
28
+ model_config = ConfigDict(populate_by_name=True)
29
+
30
+ name: str
31
+ query: str
32
+
33
+
34
+ class SavedQueryUpdate(BaseModel):
35
+ """Partial update for a saved query.
36
+
37
+ All fields optional. Dump with ``exclude_none=True`` so the POST body only
38
+ carries the fields the caller actually wants to change; YouTrack preserves
39
+ omitted fields (live-verified on ``vod.youtrack.cloud`` 2026-04-05).
40
+ """
41
+
42
+ model_config = ConfigDict(populate_by_name=True)
43
+
44
+ name: str | None = None
45
+ query: str | None = None
46
+
47
+
48
+ class QueryValidationResult(BaseModel):
49
+ """Result of validating a query string against the YouTrack search API."""
50
+
51
+ valid: bool
52
+ query: str
53
+ error: str | None = None
54
+ suggestion: str | None = None
kd/models/search.py ADDED
@@ -0,0 +1,20 @@
1
+ """Search result model for cross-entity search."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class SearchResult(BaseModel):
9
+ """Normalized result from cross-entity search.
10
+
11
+ Each result carries a type discriminator so consumers can identify
12
+ the source entity kind without inspecting the id format.
13
+ """
14
+
15
+ model_config = ConfigDict(populate_by_name=True)
16
+
17
+ type: str
18
+ id: str
19
+ title: str
20
+ project: str
kd/models/tag.py ADDED
@@ -0,0 +1,30 @@
1
+ """Tag models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from kd.models.common import IdEntity, NamedEntity
8
+
9
+
10
+ class TagColor(BaseModel):
11
+ """Tag color with hex foreground/background values."""
12
+
13
+ model_config = ConfigDict(populate_by_name=True)
14
+
15
+ id: str
16
+ background: str | None = None
17
+ foreground: str | None = None
18
+
19
+
20
+ class Tag(IdEntity):
21
+ """YouTrack tag.
22
+
23
+ Tags are shared entities that can be applied to both issues and articles.
24
+ The ``owner`` field is populated when detail fields are requested.
25
+ """
26
+
27
+ name: str
28
+ owner: NamedEntity | None = None
29
+ color: TagColor | None = None
30
+ untag_on_resolve: bool = Field(default=False, alias="untagOnResolve")
kd/models/user.py ADDED
@@ -0,0 +1,42 @@
1
+ """User models (Hub API entities)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from kd.models.common import IdEntity, NamedEntity
8
+
9
+
10
+ class User(IdEntity):
11
+ """User representation (works for both YT and Hub API responses)."""
12
+
13
+ login: str
14
+ name: str
15
+ email: str | None = None
16
+ ring_id: str | None = Field(default=None, alias="ringId")
17
+ banned: bool = False
18
+ guest: bool = False
19
+
20
+
21
+ class UserCreate(BaseModel):
22
+ """Input for creating a user via Hub API."""
23
+
24
+ model_config = ConfigDict(populate_by_name=True)
25
+
26
+ login: str
27
+ name: str
28
+ email: str
29
+
30
+
31
+ class Group(IdEntity):
32
+ """User group representation."""
33
+
34
+ name: str
35
+ users_count: int | None = Field(default=None, alias="usersCount")
36
+
37
+
38
+ class ProjectTeam(IdEntity):
39
+ """Project team (auto-created group per project)."""
40
+
41
+ name: str
42
+ project: NamedEntity | None = None
kd/models/work_item.py ADDED
@@ -0,0 +1,59 @@
1
+ """Work item (time tracking) models.
2
+
3
+ Note: REST API requires type ID, not name. The SDK resolves names to IDs.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ from kd.models.common import IdEntity, NamedEntity
11
+
12
+
13
+ class WorkItemType(IdEntity):
14
+ """Work item type (Development, Testing, Documentation, etc.)."""
15
+
16
+ name: str
17
+ auto_attached: bool = Field(default=False, alias="autoAttached")
18
+
19
+
20
+ class Duration(BaseModel):
21
+ """Duration in minutes, matching the API's duration object."""
22
+
23
+ model_config = ConfigDict(populate_by_name=True)
24
+
25
+ minutes: int
26
+
27
+
28
+ class WorkItem(IdEntity):
29
+ """Time entry on an issue."""
30
+
31
+ model_config = ConfigDict(populate_by_name=True)
32
+
33
+ duration: Duration
34
+ text: str | None = None
35
+ type: WorkItemType | None = None
36
+ author: NamedEntity | None = None
37
+ date: int | None = None
38
+
39
+
40
+ class WorkItemCreate(BaseModel):
41
+ """Input for logging work."""
42
+
43
+ model_config = ConfigDict(populate_by_name=True)
44
+
45
+ duration: Duration
46
+ text: str | None = None
47
+ type: IdEntity | None = None
48
+ date: int | None = None
49
+
50
+
51
+ class WorkItemUpdate(BaseModel):
52
+ """Input for editing a work item. All fields optional."""
53
+
54
+ model_config = ConfigDict(populate_by_name=True)
55
+
56
+ duration: Duration | None = None
57
+ text: str | None = None
58
+ type: IdEntity | None = None
59
+ date: int | None = None