kd-cli 2.2.0__py3-none-any.whl → 2.2.2__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/api/youtrack.py CHANGED
@@ -24,6 +24,7 @@ from kd.models import (
24
24
  Attachment,
25
25
  Comment,
26
26
  CommentCreate,
27
+ CustomField,
27
28
  FieldSet,
28
29
  Issue,
29
30
  IssueCreate,
@@ -38,6 +39,28 @@ from kd.models import (
38
39
  WorkItemType,
39
40
  )
40
41
 
42
+
43
+ def _custom_field_payload(field: CustomField) -> dict[str, Any]:
44
+ payload = field.model_dump(by_alias=True, exclude_none=True)
45
+ if field.value is None:
46
+ payload["value"] = None
47
+ return payload
48
+
49
+
50
+ def _issue_create_payload(issue: IssueCreate) -> dict[str, Any]:
51
+ payload = issue.model_dump(by_alias=True, exclude_none=True)
52
+ if issue.custom_fields is not None:
53
+ payload["customFields"] = [_custom_field_payload(field) for field in issue.custom_fields]
54
+ return payload
55
+
56
+
57
+ def _issue_update_payload(update: IssueUpdate) -> dict[str, Any]:
58
+ payload = update.model_dump(by_alias=True, exclude_none=True)
59
+ if update.custom_fields is not None:
60
+ payload["customFields"] = [_custom_field_payload(field) for field in update.custom_fields]
61
+ return payload
62
+
63
+
41
64
  # Default field selections for common queries
42
65
  _PROJECT_LIST_FIELDS = str(
43
66
  FieldSet(
@@ -226,7 +249,7 @@ class YouTrackAPI:
226
249
  def create_issue(self, issue: IssueCreate) -> Issue:
227
250
  data = self._http.post(
228
251
  "/issues",
229
- json=issue.model_dump(by_alias=True, exclude_none=True),
252
+ json=_issue_create_payload(issue),
230
253
  fields=_ISSUE_FIELDS,
231
254
  )
232
255
  return Issue.model_validate(data)
@@ -234,7 +257,7 @@ class YouTrackAPI:
234
257
  def update_issue(self, issue_id: str, update: IssueUpdate) -> Issue:
235
258
  data = self._http.post(
236
259
  f"/issues/{issue_id}",
237
- json=update.model_dump(by_alias=True, exclude_none=True),
260
+ json=_issue_update_payload(update),
238
261
  fields=_ISSUE_FIELDS,
239
262
  )
240
263
  return Issue.model_validate(data)
kd/cli/main.py CHANGED
@@ -30,6 +30,7 @@ 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
32
  from kd.exceptions import KdError
33
+ from kd.operating_guide import load_guide
33
34
 
34
35
  # Global options — single source of truth for extraction, help, and reference.
35
36
  GLOBAL_OPTIONS: list[OptionDeclaration] = [
@@ -54,6 +55,13 @@ GLOBAL_OPTIONS: list[OptionDeclaration] = [
54
55
  type=bool,
55
56
  help_text="Print full command reference (LLM-friendly)",
56
57
  ),
58
+ OptionDeclaration(
59
+ name="guide",
60
+ short_name="G",
61
+ layer=OptionLayer.GLOBAL,
62
+ type=bool,
63
+ help_text="Print embedded operating guide (LLM-friendly)",
64
+ ),
57
65
  OptionDeclaration(
58
66
  name="debug",
59
67
  short_name="D",
@@ -158,6 +166,10 @@ def main(argv: list[str] | None = None) -> None:
158
166
  print(f"kd {__version__}")
159
167
  return
160
168
 
169
+ if global_opts.get("guide"):
170
+ print(load_guide(), end="")
171
+ return
172
+
161
173
  # Alias expansion: detect `alias <name>` and expand before parsing.
162
174
  # Config is loaded early only when needed; stored for reuse later.
163
175
  resolved: ResolvedConfig | None = None
kd/client.py CHANGED
@@ -279,11 +279,7 @@ class FieldSchema:
279
279
  )
280
280
 
281
281
  validated_value = self._validate_field_value(field, value)
282
- field_value = (
283
- {"login": validated_value}
284
- if self._is_user_field(field)
285
- else {"name": validated_value}
286
- )
282
+ field_value = self._field_value_payload(field, validated_value)
287
283
  self._debug.debug(
288
284
  "fields",
289
285
  f"resolved '{slot}' -> field '{field.name}' ($type={issue_type})",
@@ -350,9 +346,7 @@ class FieldSchema:
350
346
  f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
351
347
  )
352
348
  validated_value = self._validate_field_value(field, value)
353
- field_value = (
354
- {"login": validated_value} if self._is_user_field(field) else {"name": validated_value}
355
- )
349
+ field_value = self._field_value_payload(field, validated_value)
356
350
  self._debug.debug(
357
351
  "fields",
358
352
  f"resolved generic '{field_name}' -> field '{field.name}' "
@@ -392,16 +386,36 @@ class FieldSchema:
392
386
  """Return True if this field is a user-type field (user[1])."""
393
387
  return field.field_type_id.startswith("user")
394
388
 
389
+ def _field_value_payload(
390
+ self,
391
+ field: ProjectCustomField,
392
+ value: str | None,
393
+ ) -> dict[str, str] | None:
394
+ """Build the YouTrack issue-field value payload.
395
+
396
+ ``None`` is a deliberate clear operation and must serialize as
397
+ ``"value": null`` in the final issue mutation payload.
398
+ """
399
+ if value is None:
400
+ return None
401
+ return {"login": value} if self._is_user_field(field) else {"name": value}
402
+
395
403
  def _validate_field_value(
396
404
  self,
397
405
  field: ProjectCustomField,
398
406
  value: str,
399
- ) -> str:
407
+ ) -> str | None:
400
408
  """Validate a value against the field's bundle.
401
409
 
402
- Returns the canonical value name (case-corrected).
410
+ Returns the canonical value name (case-corrected), or ``None``
411
+ when an optional field should be cleared.
403
412
  Raises ValidationError with allowed values on mismatch.
404
413
  """
414
+ if value == "":
415
+ if field.can_be_empty:
416
+ return None
417
+ raise ValidationError(f"Field '{field.name}' is required and cannot be cleared.")
418
+
405
419
  if self._is_user_field(field):
406
420
  return value # User fields: server validates against group members
407
421
  if not field.bundle_values:
kd/operating_guide.py ADDED
@@ -0,0 +1,20 @@
1
+ """Operating guide loader.
2
+
3
+ Reads the bundled ``kd/resources/operating-guide.md`` for ``kd --guide``.
4
+ The guide carries durable operating doctrine (product boundary, primer
5
+ workflow, cost classes, YouTrack gotchas) — a complement to the
6
+ syntax-focused ``kd --reference``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.resources
12
+
13
+
14
+ def load_guide() -> str:
15
+ """Read the bundled operating guide as text."""
16
+ return (
17
+ importlib.resources.files("kd.resources")
18
+ .joinpath("operating-guide.md")
19
+ .read_text(encoding="utf-8")
20
+ )
kd/resources/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.2.2] — 2026-04-18
8
+
9
+ ### Fixed
10
+ - `kd /issue update ID --field "NAME="` now clears optional custom fields by sending `value: null` to YouTrack. Required fields reject empty values with a clear client-side error instead of the generic "invalid value" bundle message (KDCLI-81).
11
+
12
+ ## [2.2.1] — 2026-04-18
13
+
14
+ ### Added
15
+ - `kd --guide` / `-G` prints an embedded operating guide covering kd's product boundary, primer workflow, cost classes, and YouTrack gotchas — the doctrine complement to the syntax-focused `kd --reference`. Runs without a connection or working config, so it's safe in fresh or broken-config environments (KDCLI-94).
16
+
7
17
  ## [2.2.0] — 2026-04-18
8
18
 
9
19
  Milestone 006 — tag discovery and taxonomy hygiene v1. Eight sequenced sessions that turn tags into a load-bearing cross-entity discovery primitive: `/issue` tag parity with `/article`, multi-tag intersection on `/tag issues`, the `/tag entities` cross-kind verb, `/tag list --prefix|--namespace` taxonomy introspection, and workspace-pinned tags on the primer consumed as an implicit OR scope.
@@ -97,6 +97,8 @@ Issue custom fields are project-specific and need `$type` discriminators on muta
97
97
  kd /project/field list KDCLI
98
98
  ```
99
99
 
100
+ Clear optional custom fields with an empty value, for example `kd /issue update KDCLI-42 --field "Milestone="`. Required fields reject empty values before any upstream write.
101
+
100
102
  Attachment metadata download URLs may start with `/api/`. kd handles this internally on download commands; do not assume the raw URL can be appended unchanged to an API base URL in custom scripts.
101
103
 
102
104
  Tag existence, tag visibility, and tag sharing rights are different concerns. A visible tag may not be usable for every operation, and sharing settings are provider-specific security posture, not just display metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -1,7 +1,7 @@
1
1
  kd/__init__.py,sha256=hzqgTR9bMhfiHP3qoh3Z-0uJ4gTkPIIzEAvqSJp_gKU,993
2
2
  kd/article_marker.py,sha256=qfVw1Id9PEezYszq0yn6fJgY88-fz98OdNnF9G49xPA,12700
3
3
  kd/auth.py,sha256=uNq8wOZBPBa4-uYurZCBu2kSDy_eZlIu2yKzNjTnIp0,5459
4
- kd/client.py,sha256=8dkYK1OdoBwZOjsBiIDjOoMAmmpYhOKxyQZDA9jAfYw,85013
4
+ kd/client.py,sha256=TisHwLEdJZJGhNA4haI1v-j0xKXoCubsL8cAzPXLBj8,85576
5
5
  kd/config.py,sha256=AvyN0GH-cQAVldVYlrFYCQq6a6QThh-8upQnUzB9RAQ,53714
6
6
  kd/config_read.py,sha256=tr3b_JGq3VYt8m-P9QJDS4s8E4_nS54I3RMOSl4D3EQ,7265
7
7
  kd/config_schema.py,sha256=H4PlVl5CorrztDEX3E4Y-4QHK5o-fpcPZsWIYybnJzM,7943
@@ -9,6 +9,7 @@ kd/connection.py,sha256=oHjT4i4dbnV47ZPpbw1eGmNPLM9PR2iO-xc_pxLwa2g,6728
9
9
  kd/debug.py,sha256=JvhBUiUvCzOSsH-ONfmKLnLHSaphbveAq_mbf-Y0OQY,3322
10
10
  kd/exceptions.py,sha256=-dtT9sD_qGXEtquoDQZ7wlm9oRTN3pDqxoIvJ-TPYg4,6533
11
11
  kd/history.py,sha256=osLPgcgK1dwDU5Af4LoNDMpmh2M6zyvJJO3TUoGtJPQ,5701
12
+ kd/operating_guide.py,sha256=PvBolNHiSMKGxgyCQbx3cDPxd9Wq6zvURNiuzJ9Mo1w,567
12
13
  kd/primer.py,sha256=49PCHDtfTgWtBXTenGg_rjmecfQ7cwOPTp4_lCJmNRs,9714
13
14
  kd/query_parser.py,sha256=y4_xQ5eeeq9oeiUdCk4YdJ-MyJ5e-Ox72JWWouo0uNI,8205
14
15
  kd/release_notes.py,sha256=lrBIHvByCX-dojEljQqf8cbIAvoTOc_3ZV8Am_3wnkc,3294
@@ -18,10 +19,10 @@ kd/api/http.py,sha256=_lsWyvSktlKqb3v3Is-4-3dw_G-2ILquYvfXGEwyajo,8804
18
19
  kd/api/hub.py,sha256=zHEDr9-_SqqkVmC3Duu87Yx4bbLrEh0NbSSRhrD25ug,2364
19
20
  kd/api/saved_queries.py,sha256=5GhMqJeeTvcooC-07T6RU-WIxMGbRyMJKcGIGx3ihTk,2579
20
21
  kd/api/tags.py,sha256=P9gA-rfqinshgNFkkb0KLz0iIZR3rf-f-H1RwR63GNg,3275
21
- kd/api/youtrack.py,sha256=l3HeuDxQRc2p6D0O4DhLQlN_hYu3PzubNlsUFUNjt7M,14838
22
+ kd/api/youtrack.py,sha256=YuVOExRvcy676gn6bA8q65QgaWYHtrPPCZWW3vKKZHU,15605
22
23
  kd/cli/__init__.py,sha256=Pvvndapu834rERT6IhFb5hqzSgr0V-fctfHCcz8uT0U,138
23
24
  kd/cli/input.py,sha256=u3gLQfRg0R5VS2s45s2q0shplussssSHP5TdA7IliK0,1350
24
- kd/cli/main.py,sha256=501IRnTyunSzxGaPRcJFnkfbOMJ3504P7oyhJ-9kmkU,21974
25
+ kd/cli/main.py,sha256=Ry8oiafbEiltpwnN_bGVoOEggSrOGoGfioeLrqxg6mY,22297
25
26
  kd/cli/output.py,sha256=O8qfOSV-zwsGu79snQwizpOi1KoAzbepF7u2tIoqIIM,17775
26
27
  kd/cli/tree.py,sha256=1fMQ6QZNL6LGt7GzsbxR-ECzRyp9wTxP2Ca7l5Hvc4A,1883
27
28
  kd/cli/core/__init__.py,sha256=cwxvTg-ABcTHnnC86UDjrNfxT7ekRFnn2p_J12QoFG4,588
@@ -57,15 +58,15 @@ kd/models/tag.py,sha256=imrqqi-M-5aepel5pk81DmsRu0W6pbNcPRkZYVHYHl8,742
57
58
  kd/models/tag_entities.py,sha256=GhEufLlG_pN_p-5H-mipzodeBg0n0ovhlsMpWKrIrHY,1335
58
59
  kd/models/user.py,sha256=ICu10OdvZJVuXtFC5XUYreUdJRpDmfQbILR1bjPDZ6k,912
59
60
  kd/models/work_item.py,sha256=fJXcSiTQQmXa9d4IuFYmcmC1qrlupNJjtYFW1tzYv6A,1394
60
- kd/resources/CHANGELOG.md,sha256=YnW_WHO9JspfstT_IHKaPok91785iYvx0Bb-XOOSKwc,12227
61
+ kd/resources/CHANGELOG.md,sha256=Waf9fGCccc7TIsXCYOrfSkJohyiWSRkJ_TWHgB-wLj4,12866
61
62
  kd/resources/__init__.py,sha256=yHAagxZ8q-eFXM0-mvWkafl_zQWDmaq7V2d_S8BAd0k,58
62
63
  kd/resources/command-synonyms.yaml,sha256=KW4hT9YSCB1rw_k51fNRf5XhCq-olic-JS_VMi5ufuY,435
63
64
  kd/resources/config-template.yaml,sha256=PszXHqT9-iYEaWOk995-xecCsX9kDi8Un2j2l-pNVKM,1616
64
65
  kd/resources/error-messages.yaml,sha256=fHzMW0vlNDaudEWMOMk_mbxZoGQKGarBKtUfk_Bpcn0,3857
65
- kd/resources/operating-guide.md,sha256=wLWvOzrSven9EB6XNktmIf34zXcQ0tYbiPqQsKxT1QQ,12390
66
- kd_cli-2.2.0.dist-info/licenses/LICENSE,sha256=snwwWzfIuTq72ybFaCbx-o5n8SXhNxPMgUjqSSnSS8U,1071
67
- kd_cli-2.2.0.dist-info/METADATA,sha256=S5ZujYKr5zLdz2pw7u9znceXsSYNEfbovK3-piXkrKo,7168
68
- kd_cli-2.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
69
- kd_cli-2.2.0.dist-info/entry_points.txt,sha256=njF_5vmjE3couro9XU8CRuqQzpp-VqmAmq63bmxZeqo,40
70
- kd_cli-2.2.0.dist-info/top_level.txt,sha256=-ZAP5fwCL_0Wess0ja8bEYr1IjR8LfrEuBtjEDxD8XU,3
71
- kd_cli-2.2.0.dist-info/RECORD,,
66
+ kd/resources/operating-guide.md,sha256=nkgmq-IYeCn9mbNfTqW9l5_4PZ4GDXlmeC-U21C0QBk,12566
67
+ kd_cli-2.2.2.dist-info/licenses/LICENSE,sha256=snwwWzfIuTq72ybFaCbx-o5n8SXhNxPMgUjqSSnSS8U,1071
68
+ kd_cli-2.2.2.dist-info/METADATA,sha256=ct7DYeMOckJGk0v_VA0l0SEou2AkNM1t9ib3Au6WAGk,7168
69
+ kd_cli-2.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
70
+ kd_cli-2.2.2.dist-info/entry_points.txt,sha256=njF_5vmjE3couro9XU8CRuqQzpp-VqmAmq63bmxZeqo,40
71
+ kd_cli-2.2.2.dist-info/top_level.txt,sha256=-ZAP5fwCL_0Wess0ja8bEYr1IjR8LfrEuBtjEDxD8XU,3
72
+ kd_cli-2.2.2.dist-info/RECORD,,
File without changes