kd-cli 2.2.1__tar.gz → 2.2.2__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 (78) hide show
  1. {kd_cli-2.2.1/kd_cli.egg-info → kd_cli-2.2.2}/PKG-INFO +1 -1
  2. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/youtrack.py +25 -2
  3. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/client.py +24 -10
  4. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/CHANGELOG.md +5 -0
  5. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/operating-guide.md +2 -0
  6. {kd_cli-2.2.1 → kd_cli-2.2.2/kd_cli.egg-info}/PKG-INFO +1 -1
  7. {kd_cli-2.2.1 → kd_cli-2.2.2}/pyproject.toml +1 -1
  8. {kd_cli-2.2.1 → kd_cli-2.2.2}/LICENSE +0 -0
  9. {kd_cli-2.2.1 → kd_cli-2.2.2}/MANIFEST.in +0 -0
  10. {kd_cli-2.2.1 → kd_cli-2.2.2}/README.md +0 -0
  11. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/__init__.py +0 -0
  12. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/__init__.py +0 -0
  13. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/articles.py +0 -0
  14. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/http.py +0 -0
  15. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/hub.py +0 -0
  16. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/saved_queries.py +0 -0
  17. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/tags.py +0 -0
  18. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/article_marker.py +0 -0
  19. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/auth.py +0 -0
  20. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/__init__.py +0 -0
  21. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/__init__.py +0 -0
  22. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/command_base.py +0 -0
  23. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/command_parser.py +0 -0
  24. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/namespaces.py +0 -0
  25. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/option_registry.py +0 -0
  26. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/input.py +0 -0
  27. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/main.py +0 -0
  28. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/__init__.py +0 -0
  29. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/alias/__init__.py +0 -0
  30. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/article/__init__.py +0 -0
  31. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/init.py +0 -0
  32. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/issue/__init__.py +0 -0
  33. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/primer/__init__.py +0 -0
  34. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/project/__init__.py +0 -0
  35. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/query/__init__.py +0 -0
  36. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/release_notes.py +0 -0
  37. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/search/__init__.py +0 -0
  38. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/system/__init__.py +0 -0
  39. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/tag/__init__.py +0 -0
  40. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/time/__init__.py +0 -0
  41. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/user/__init__.py +0 -0
  42. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/output.py +0 -0
  43. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/tree.py +0 -0
  44. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/config.py +0 -0
  45. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/config_read.py +0 -0
  46. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/config_schema.py +0 -0
  47. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/connection.py +0 -0
  48. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/debug.py +0 -0
  49. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/exceptions.py +0 -0
  50. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/history.py +0 -0
  51. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/__init__.py +0 -0
  52. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/article.py +0 -0
  53. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/assist.py +0 -0
  54. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/attachment.py +0 -0
  55. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/comment.py +0 -0
  56. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/common.py +0 -0
  57. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/issue.py +0 -0
  58. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/project.py +0 -0
  59. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/saved_query.py +0 -0
  60. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/search.py +0 -0
  61. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/tag.py +0 -0
  62. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/tag_entities.py +0 -0
  63. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/user.py +0 -0
  64. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/work_item.py +0 -0
  65. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/operating_guide.py +0 -0
  66. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/primer.py +0 -0
  67. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/query_parser.py +0 -0
  68. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/release_notes.py +0 -0
  69. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/__init__.py +0 -0
  70. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/command-synonyms.yaml +0 -0
  71. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/config-template.yaml +0 -0
  72. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/error-messages.yaml +0 -0
  73. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/SOURCES.txt +0 -0
  74. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/dependency_links.txt +0 -0
  75. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/entry_points.txt +0 -0
  76. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/requires.txt +0 -0
  77. {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/top_level.txt +0 -0
  78. {kd_cli-2.2.1 → kd_cli-2.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.1
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
@@ -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)
@@ -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:
@@ -4,6 +4,11 @@ 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
+
7
12
  ## [2.2.1] — 2026-04-18
8
13
 
9
14
  ### Added
@@ -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.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kd-cli"
7
- version = "2.2.1"
7
+ version = "2.2.2"
8
8
  description = "Knowledge Dispatcher for issue trackers and knowledge bases"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes