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.
- {kd_cli-2.2.1/kd_cli.egg-info → kd_cli-2.2.2}/PKG-INFO +1 -1
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/youtrack.py +25 -2
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/client.py +24 -10
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/CHANGELOG.md +5 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/operating-guide.md +2 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.2.1 → kd_cli-2.2.2}/pyproject.toml +1 -1
- {kd_cli-2.2.1 → kd_cli-2.2.2}/LICENSE +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/MANIFEST.in +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/README.md +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/articles.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/http.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/hub.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/api/tags.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/article_marker.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/auth.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/input.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/main.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/article/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/output.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/cli/tree.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/config.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/config_read.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/config_schema.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/connection.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/debug.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/exceptions.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/history.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/article.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/assist.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/attachment.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/comment.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/common.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/issue.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/project.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/saved_query.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/search.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/tag.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/user.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/models/work_item.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/operating_guide.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/primer.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/query_parser.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/release_notes.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/__init__.py +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.2.1 → kd_cli-2.2.2}/setup.cfg +0 -0
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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
|