kd-cli 2.0.6__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {kd_cli-2.0.6 → kd_cli-2.1.0}/MANIFEST.in +1 -1
  2. {kd_cli-2.0.6/kd_cli.egg-info → kd_cli-2.1.0}/PKG-INFO +5 -1
  3. {kd_cli-2.0.6 → kd_cli-2.1.0}/README.md +2 -0
  4. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/main.py +22 -6
  5. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/__init__.py +2 -0
  6. kd_cli-2.1.0/kd/cli/namespaces/init.py +61 -0
  7. kd_cli-2.1.0/kd/cli/namespaces/release_notes.py +116 -0
  8. kd_cli-2.1.0/kd/cli/namespaces/system/__init__.py +1161 -0
  9. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/config.py +544 -109
  10. kd_cli-2.1.0/kd/config_schema.py +266 -0
  11. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/connection.py +44 -12
  12. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/exceptions.py +4 -0
  13. kd_cli-2.1.0/kd/release_notes.py +111 -0
  14. kd_cli-2.1.0/kd/resources/CHANGELOG.md +54 -0
  15. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/resources/config-template.yaml +3 -15
  16. {kd_cli-2.0.6 → kd_cli-2.1.0/kd_cli.egg-info}/PKG-INFO +5 -1
  17. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd_cli.egg-info/SOURCES.txt +4 -0
  18. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd_cli.egg-info/requires.txt +1 -0
  19. {kd_cli-2.0.6 → kd_cli-2.1.0}/pyproject.toml +4 -2
  20. kd_cli-2.0.6/kd/cli/namespaces/init.py +0 -42
  21. kd_cli-2.0.6/kd/cli/namespaces/system/__init__.py +0 -528
  22. {kd_cli-2.0.6 → kd_cli-2.1.0}/LICENSE +0 -0
  23. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/__init__.py +0 -0
  24. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/api/__init__.py +0 -0
  25. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/api/articles.py +0 -0
  26. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/api/http.py +0 -0
  27. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/api/hub.py +0 -0
  28. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/api/saved_queries.py +0 -0
  29. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/api/tags.py +0 -0
  30. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/api/youtrack.py +0 -0
  31. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/article_marker.py +0 -0
  32. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/auth.py +0 -0
  33. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/__init__.py +0 -0
  34. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/core/__init__.py +0 -0
  35. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/core/command_base.py +0 -0
  36. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/core/command_parser.py +0 -0
  37. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/core/namespaces.py +0 -0
  38. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/core/option_registry.py +0 -0
  39. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/input.py +0 -0
  40. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/alias/__init__.py +0 -0
  41. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/article/__init__.py +0 -0
  42. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/issue/__init__.py +0 -0
  43. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/primer/__init__.py +0 -0
  44. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/project/__init__.py +0 -0
  45. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/query/__init__.py +0 -0
  46. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/search/__init__.py +0 -0
  47. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/tag/__init__.py +0 -0
  48. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/time/__init__.py +0 -0
  49. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/namespaces/user/__init__.py +0 -0
  50. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/output.py +0 -0
  51. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/cli/tree.py +0 -0
  52. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/client.py +0 -0
  53. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/debug.py +0 -0
  54. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/history.py +0 -0
  55. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/__init__.py +0 -0
  56. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/article.py +0 -0
  57. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/assist.py +0 -0
  58. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/attachment.py +0 -0
  59. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/comment.py +0 -0
  60. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/common.py +0 -0
  61. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/issue.py +0 -0
  62. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/project.py +0 -0
  63. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/saved_query.py +0 -0
  64. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/search.py +0 -0
  65. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/tag.py +0 -0
  66. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/user.py +0 -0
  67. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/models/work_item.py +0 -0
  68. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/primer.py +0 -0
  69. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/query_parser.py +0 -0
  70. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/resources/__init__.py +0 -0
  71. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/resources/command-synonyms.yaml +0 -0
  72. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd/resources/error-messages.yaml +0 -0
  73. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd_cli.egg-info/dependency_links.txt +0 -0
  74. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd_cli.egg-info/entry_points.txt +0 -0
  75. {kd_cli-2.0.6 → kd_cli-2.1.0}/kd_cli.egg-info/top_level.txt +0 -0
  76. {kd_cli-2.0.6 → kd_cli-2.1.0}/setup.cfg +0 -0
@@ -3,7 +3,7 @@ include LICENSE
3
3
  include pyproject.toml
4
4
  include MANIFEST.in
5
5
 
6
- recursive-include kd *.py *.yaml
6
+ recursive-include kd *.py *.yaml *.md
7
7
 
8
8
  prune tests
9
9
  prune .kd
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.0.6
3
+ Version: 2.1.0
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://pypi.org/project/kd-cli/
8
8
  Project-URL: Issues, https://github.com/IntellectSolutionsCom/kd-cli/issues
9
9
  Project-URL: Source, https://github.com/IntellectSolutionsCom/kd-cli
10
+ Project-URL: Changelog, https://github.com/IntellectSolutionsCom/kd-cli/blob/main/kd/resources/CHANGELOG.md
10
11
  Keywords: youtrack,cli,sdk,knowledge-management,knowledge-network
11
12
  Classifier: Development Status :: 4 - Beta
12
13
  Classifier: Environment :: Console
@@ -22,6 +23,7 @@ Requires-Python: >=3.11
22
23
  Description-Content-Type: text/markdown
23
24
  License-File: LICENSE
24
25
  Requires-Dist: httpx>=0.27
26
+ Requires-Dist: packaging>=23.0
25
27
  Requires-Dist: pydantic>=2.0
26
28
  Requires-Dist: pyyaml>=6.0
27
29
  Requires-Dist: tabulate>=0.9
@@ -80,6 +82,8 @@ uv tool install kd-cli
80
82
 
81
83
  The PyPI distribution name is `kd-cli` (two-letter `kd` is reserved by PyPI policy). The installed command, the import name, and the CLI entry point are all `kd`.
82
84
 
85
+ Release notes for the installed version: `kd /release-notes`. The canonical changelog lives at [`kd/resources/CHANGELOG.md`](kd/resources/CHANGELOG.md).
86
+
83
87
  ## Configure
84
88
 
85
89
  ```bash
@@ -43,6 +43,8 @@ uv tool install kd-cli
43
43
 
44
44
  The PyPI distribution name is `kd-cli` (two-letter `kd` is reserved by PyPI policy). The installed command, the import name, and the CLI entry point are all `kd`.
45
45
 
46
+ Release notes for the installed version: `kd /release-notes`. The canonical changelog lives at [`kd/resources/CHANGELOG.md`](kd/resources/CHANGELOG.md).
47
+
46
48
  ## Configure
47
49
 
48
50
  ```bash
@@ -29,7 +29,7 @@ from kd.client import YouTrackClient
29
29
  from kd.config import ResolvedConfig, resolve_command_defaults, resolve_config
30
30
  from kd.connection import ConnectionContext, resolve_connection
31
31
  from kd.debug import DebugLevel, DebugLog, init_debug_log
32
- from kd.exceptions import APIError, KdError
32
+ from kd.exceptions import APIError, ConfigError, KdError
33
33
 
34
34
  # Global options — single source of truth for extraction, help, and reference.
35
35
  GLOBAL_OPTIONS: list[OptionDeclaration] = [
@@ -121,10 +121,26 @@ def expand_toplevel_alias(
121
121
  def _print_error(error: KdError) -> None:
122
122
  """Print error with optional suggestion to stderr."""
123
123
  print(f"Error: {error}", file=sys.stderr)
124
- if isinstance(error, APIError) and error.suggestion:
124
+ if isinstance(error, (APIError, ConfigError)) and error.suggestion:
125
125
  print(f"Hint: {error.suggestion}", file=sys.stderr)
126
126
 
127
127
 
128
+ def _resolve_config_or_exit(
129
+ flags: dict[str, str] | None,
130
+ debug_log: DebugLog,
131
+ ) -> ResolvedConfig:
132
+ """Wrap :func:`resolve_config` so config failures exit cleanly.
133
+
134
+ Unexpected (non-``KdError``) exceptions are not caught — they must
135
+ remain visible with their traceback.
136
+ """
137
+ try:
138
+ return resolve_config(flags=flags, debug_log=debug_log)
139
+ except KdError as e:
140
+ _print_error(e)
141
+ sys.exit(1)
142
+
143
+
128
144
  def main(argv: list[str] | None = None) -> None:
129
145
  """CLI entry point."""
130
146
  args = argv if argv is not None else sys.argv[1:]
@@ -146,7 +162,7 @@ def main(argv: list[str] | None = None) -> None:
146
162
  # Config is loaded early only when needed; stored for reuse later.
147
163
  resolved: ResolvedConfig | None = None
148
164
  if len(remaining) >= 2 and remaining[0] == "alias" and remaining[1] not in _ALIAS_SUBCOMMANDS:
149
- resolved = resolve_config(flags=config_flags, debug_log=debug_log)
165
+ resolved = _resolve_config_or_exit(config_flags, debug_log)
150
166
  remaining = expand_alias(remaining, resolved)
151
167
 
152
168
  # Build registry and parser
@@ -155,7 +171,7 @@ def main(argv: list[str] | None = None) -> None:
155
171
 
156
172
  if global_opts.get("reference"):
157
173
  if resolved is None:
158
- resolved = resolve_config(flags=config_flags, debug_log=debug_log)
174
+ resolved = _resolve_config_or_exit(config_flags, debug_log)
159
175
  _show_reference(registry, config=resolved)
160
176
  return
161
177
  parser = CommandParser(registry)
@@ -195,7 +211,7 @@ def main(argv: list[str] | None = None) -> None:
195
211
  # check whether it matches a configured alias and expand it.
196
212
  if unresolved.namespace is registry.root:
197
213
  if resolved is None:
198
- resolved = resolve_config(flags=config_flags, debug_log=debug_log)
214
+ resolved = _resolve_config_or_exit(config_flags, debug_log)
199
215
  expanded = expand_toplevel_alias(unresolved.command_name, remaining[1:], resolved)
200
216
  if expanded is not None:
201
217
  try:
@@ -229,7 +245,7 @@ def main(argv: list[str] | None = None) -> None:
229
245
 
230
246
  # Resolve config from flags + env + local + global (reuse if loaded for alias expansion)
231
247
  if resolved is None:
232
- resolved = resolve_config(flags=config_flags, debug_log=debug_log)
248
+ resolved = _resolve_config_or_exit(config_flags, debug_log)
233
249
 
234
250
  # Debug config key: if config says debug=true and no -D on CLI, enable level 1
235
251
  if resolved.debug and not global_opts.get("debug"):
@@ -12,6 +12,7 @@ from kd.cli.namespaces.issue import create_issue_namespace
12
12
  from kd.cli.namespaces.primer import create_primer_namespace
13
13
  from kd.cli.namespaces.project import create_project_namespace
14
14
  from kd.cli.namespaces.query import create_query_namespace
15
+ from kd.cli.namespaces.release_notes import ReleaseNotesCommand
15
16
  from kd.cli.namespaces.search import SearchCommand
16
17
  from kd.cli.namespaces.system import create_system_namespace
17
18
  from kd.cli.namespaces.tag import create_tag_namespace
@@ -22,6 +23,7 @@ from kd.cli.namespaces.user import create_user_namespace
22
23
  def register_all_namespaces(registry: NamespaceRegistry) -> None:
23
24
  """Register all namespace trees in the registry."""
24
25
  registry.root.register_command("init", InitCommand())
26
+ registry.root.register_command("release-notes", ReleaseNotesCommand())
25
27
  registry.root.register_command("search", SearchCommand())
26
28
  registry.register_namespace(create_alias_namespace())
27
29
  registry.register_namespace(create_article_namespace())
@@ -0,0 +1,61 @@
1
+ """Init command: kd init
2
+
3
+ Scaffolds a kd config file at the local tier (.kd/config.yaml in the
4
+ current directory, with .gitignore management) or the global tier
5
+ (~/.config/kd/config.yaml, parent directory created as needed).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from kd.cli.core.command_base import Command, CommandResult
13
+ from kd.cli.core.option_registry import OptionDeclaration, OptionLayer, ResolvedOptions
14
+ from kd.config import init_project_config
15
+
16
+
17
+ class InitCommand(Command):
18
+ """Initialize project config."""
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__(
22
+ "init",
23
+ "Initialize kd config (.kd/config.yaml or ~/.config/kd/config.yaml)",
24
+ usage="kd init [URL] [--global]",
25
+ long_description=(
26
+ "Creates a config file. Default is .kd/config.yaml in the "
27
+ "current directory, with .kd/ added to .gitignore when inside "
28
+ "a git repository. With --global (-g), writes "
29
+ "~/.config/kd/config.yaml instead, creating the parent "
30
+ "directory if needed. Optionally pre-fills the YouTrack URL."
31
+ ),
32
+ examples=[
33
+ "kd init",
34
+ "kd init https://example.youtrack.cloud",
35
+ "kd init --global https://example.youtrack.cloud",
36
+ ],
37
+ see_also=["/system/config list", "/system/config which"],
38
+ )
39
+
40
+ def _register_options(self) -> None:
41
+ self.register_option(
42
+ OptionDeclaration(
43
+ name="global",
44
+ layer=OptionLayer.COMMAND,
45
+ type=bool,
46
+ short_name="g",
47
+ help_text="Scaffold global config at ~/.config/kd/config.yaml",
48
+ )
49
+ )
50
+
51
+ def execute(
52
+ self, args: list[str], options: ResolvedOptions, client: Any = None
53
+ ) -> CommandResult:
54
+ url = args[0] if args else None
55
+ use_global = options.command.get("global", False)
56
+ result = init_project_config(url=url, global_tier=use_global)
57
+ return {
58
+ "created": str(result.created),
59
+ "gitignore": result.gitignore_action,
60
+ "next": "Run 'kd /system/config keys' to list configurable paths.",
61
+ }
@@ -0,0 +1,116 @@
1
+ """Release-notes command: /release-notes
2
+
3
+ Serves the bundled `kd/resources/CHANGELOG.md`. Supports slicing by
4
+ `--version`, `--since`, or `--all`. Default returns the latest released
5
+ section.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from packaging.version import InvalidVersion
13
+
14
+ from kd.cli.core.command_base import Command, CommandResult
15
+ from kd.cli.core.option_registry import OptionDeclaration, OptionLayer, ResolvedOptions
16
+ from kd.exceptions import ValidationError
17
+ from kd.release_notes import (
18
+ filter_since,
19
+ get_version,
20
+ latest,
21
+ load_changelog,
22
+ parse_changelog,
23
+ render_section,
24
+ )
25
+
26
+
27
+ class ReleaseNotesCommand(Command):
28
+ """Print release notes from the bundled changelog."""
29
+
30
+ def __init__(self) -> None:
31
+ super().__init__(
32
+ "release-notes",
33
+ "Print release notes from the bundled changelog",
34
+ usage="kd /release-notes [VERSION] [--since X.Y.Z | --all]",
35
+ long_description=(
36
+ "Reads the bundled CHANGELOG.md and prints one or more "
37
+ "release sections. Default returns the latest released "
38
+ "section. Pass a version positionally to pin a specific "
39
+ "release, --since to slice everything strictly newer than "
40
+ "a given version, or --all for the complete file. Output "
41
+ "defaults to markdown; -o json or -o yaml emits a "
42
+ "structured list of {version, date, body} dicts for "
43
+ "programmatic consumers. Source: kd/resources/CHANGELOG.md "
44
+ "in the installed wheel."
45
+ ),
46
+ examples=[
47
+ "kd /release-notes",
48
+ "kd /release-notes 2.1.0",
49
+ "kd /release-notes --since 2.0.5",
50
+ "kd /release-notes --all -o json",
51
+ ],
52
+ see_also=["/primer show"],
53
+ )
54
+
55
+ def _register_options(self) -> None:
56
+ self.register_option(
57
+ OptionDeclaration(
58
+ name="since",
59
+ layer=OptionLayer.COMMAND,
60
+ type=str,
61
+ help_text="Return every version strictly newer than this",
62
+ )
63
+ )
64
+ self.register_option(
65
+ OptionDeclaration(
66
+ name="all",
67
+ layer=OptionLayer.COMMAND,
68
+ type=bool,
69
+ help_text="Return the complete changelog",
70
+ )
71
+ )
72
+
73
+ def execute(
74
+ self, args: list[str], options: ResolvedOptions, client: Any = None
75
+ ) -> CommandResult:
76
+ version = args[0] if args else None
77
+ since = options.command.get("since")
78
+ show_all = options.command.get("all", False)
79
+
80
+ chosen = [
81
+ flag
82
+ for flag, val in (("VERSION", version), ("--since", since), ("--all", show_all))
83
+ if val
84
+ ]
85
+ if len(chosen) > 1:
86
+ raise ValidationError(
87
+ f"VERSION, --since, and --all are mutually exclusive; got {', '.join(chosen)}"
88
+ )
89
+
90
+ sections = parse_changelog(load_changelog())
91
+
92
+ if version:
93
+ section = get_version(sections, version)
94
+ if section is None:
95
+ raise ValidationError(
96
+ f"Version {version} not found in changelog. "
97
+ f"Run 'kd /release-notes --all' to see available versions."
98
+ )
99
+ result = [section]
100
+ elif since:
101
+ try:
102
+ result = filter_since(sections, since)
103
+ except InvalidVersion as exc:
104
+ raise ValidationError(f"Invalid version '{since}': {exc}") from exc
105
+ elif show_all:
106
+ result = sections
107
+ else:
108
+ only = latest(sections)
109
+ if only is None:
110
+ raise ValidationError("Changelog has no released versions.")
111
+ result = [only]
112
+
113
+ fmt = options.output.get("output")
114
+ if fmt in ("json", "yaml"):
115
+ return [{"version": s.version, "date": s.date, "body": s.body} for s in result]
116
+ return "\n\n".join(render_section(s) for s in result)